Path: blob/master/src/packages/frontend/course/compute/terminal-command.tsx
1503 views
import { Button, Card, Input, InputNumber, List, Space } from "antd";1import { Icon } from "@cocalc/frontend/components/icon";2import { type CSSProperties, useState } from "react";3import ShowError from "@cocalc/frontend/components/error";4import { plural } from "@cocalc/util/misc";5import { getServerId } from "./students";6import type { SelectedStudents, ServersMap } from "./students";7import type { Unit } from "../store";8import type { CourseActions } from "../actions";9import { RenderOutput } from "../configuration/terminal-command";1011export function TerminalButton({ terminal, setTerminal }) {12return (13<>14<Button onClick={() => setTerminal(!terminal)}>15<Icon name="terminal" /> Terminal16</Button>17</>18);19}2021export function TerminalCommand({22style,23servers,24selected,25unit,26actions,27onClose,28}: {29style?: CSSProperties;30servers: ServersMap;31selected: SelectedStudents;32unit: Unit;33actions: CourseActions;34onClose;35}) {36const [timeout, setTimeout] = useState<number | null>(30);37const [command, setCommand] = useState<string>("");38const [running, setRunning] = useState<boolean>(false);39const [error, setError] = useState<string>("");40const [outputs, setOutputs] = useState<41{42stdout?: string;43stderr?: string;44exit_code?: number;45student_id: string;46total_time: number;47}[]48>([]);4950const runningStudentIds: string[] = Array.from(selected).filter(51(student_id) =>52servers[getServerId({ unit, student_id })]?.state == "running",53);5455const runTerminalCommand = async () => {56try {57setRunning(true);58setOutputs([]);59await actions.compute.runTerminalCommand({60setOutputs,61unit,62student_ids: runningStudentIds,63command,64timeout: timeout ?? 30,65err_on_exit: false,66});67} catch (err) {68setError(`${err}`);69} finally {70setRunning(false);71}72};7374return (75<Card76title={77<div>78<Icon name="terminal" style={{ marginRight: "5px" }} /> Run79{running ? "ning" : ""} Command on the {runningStudentIds.length}{" "}80Running Student Compute {plural(runningStudentIds.length, "Server")}81<Button onClick={onClose} style={{ float: "right" }}>82Close83</Button>84</div>85}86style={style}87>88<Space.Compact89style={{90display: "flex",91whiteSpace: "nowrap",92marginBottom: "15px",93}}94>95<Input96allowClear97disabled={running}98style={{ fontFamily: "monospace" }}99placeholder={"Command to run on compute servers..."}100value={command}101onChange={(e) => {102setCommand(e.target.value);103}}104onPressEnter={() => {105runTerminalCommand();106}}107/>108<Button109style={{ width: "6em" }}110onClick={runTerminalCommand}111disabled={running || runningStudentIds.length == 0 || !command.trim()}112>113<Icon114name={running ? "cocalc-ring" : "play"}115spin={running}116style={{ marginRight: "5px" }}117/>{" "}118Run119</Button>120</Space.Compact>121<InputNumber122disabled={running}123style={{ maxWidth: "300px" }}124value={timeout}125onChange={(t) => setTimeout(t ?? null)}126min={15}127max={60 * 60}128addonAfter={"seconds timeout"}129/>130<ShowError style={{ margin: "15px" }} error={error} setError={setError} />131{outputs.length > 0 && (132<List133size="small"134style={{ marginTop: "15px", maxHeight: "400px", overflowY: "auto" }}135bordered136dataSource={outputs}137renderItem={(output) => (138<List.Item style={{ padding: "5px 5px 5px 30px" }}>139<RenderOutput140key={output.student_id}141title={142actions.get_store()?.get_student_name(output.student_id) ??143"---"144}145stdout={output.stdout}146stderr={output.stderr}147timeout={timeout}148total_time={output.total_time}149/>150</List.Item>151)}152/>153)}154</Card>155);156}157158159