CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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