yggdrasil/pages/admin/
runner.rs1use leptos::prelude::*;
11use leptos::task::spawn_local;
12use leptos_router::{lazy_route, LazyRoute};
13
14use crate::components::admin_layout::AdminPageHead;
15use crate::components::code_runner::CodeRunner;
16use crate::components::forms::{FormLabel, FormTextarea};
17use crate::components::ui::{SegmentedControl, SegmentedItem, StatusDot};
18use crate::infra::runner_config::ResourceLimits;
19use crate::models::settings::RunnerSettings;
20use crate::routes::Route;
21
22const OVERRIDES_PLACEHOLDER: &str = "{\"timeout_secs\": 3}";
23const SUPPORTED_LANGS: &[(&str, &str)] = &[
25 ("python", "Python"),
26 ("node", "Node.js"),
27 ("go", "Go"),
28 ("rust", "Rust"),
29 ("bun", "Bun · TS"),
30];
31fn default_source(lang: &str) -> String {
33 match lang {
34 "python" => "print('Hello from author sandbox')\nfor i in range(3):\n print(f'line {i}')\n".to_string(),
35 "node" => "console.log('Hello from author sandbox');\n[0,1,2].forEach(i => console.log(`line ${i}`));\n".to_string(),
36 "go" => "package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"Hello from author sandbox\")\n\tfor i := 0; i < 3; i++ {\n\t\tfmt.Printf(\"line %d\\n\", i)\n\t}\n}\n".to_string(),
37 "rust" => "fn main() {\n println!(\"Hello from author sandbox\");\n for i in 0..3 {\n println!(\"line {}\", i);\n }\n}\n".to_string(),
38 "bun" => "const greeting: string = 'Hello from author sandbox';\nconsole.log(greeting);\n[0, 1, 2].forEach((i: number) => console.log(`line ${i}`));\n".to_string(),
40 _ => String::new(),
41 }
42}
43
44fn parse_overrides(raw: &str) -> (Option<ResourceLimits>, String) {
46 match serde_json::from_slice::<ResourceLimits>(raw.trim().as_bytes()) {
47 Ok(o) => (Some(o), String::new()),
48 Err(_) if raw.trim().is_empty() => (None, String::new()),
49 Err(_) => (None, "JSON 格式不对,已忽略".to_string()),
50 }
51}
52
53pub struct RunnerRoute;
55
56#[lazy_route]
57impl LazyRoute for RunnerRoute {
58 fn data() -> Self {
59 Self
60 }
61
62 fn view(_this: Self) -> AnyView {
63 view! { <Runner/> }.into_any()
64 }
65}
66
67#[component]
69fn Runner() -> impl IntoView {
70 let lang = RwSignal::new("python".to_string());
71 let source = RwSignal::new(default_source("python"));
73 let overrides_json = RwSignal::new(String::new());
74 let limits = RwSignal::new(None::<RunnerSettings>);
75 Effect::new(move |_| {
77 spawn_local(async move {
78 if let Ok(s) = crate::api::settings::get_runner_settings().await {
79 let _ = limits.try_set(Some(s));
80 }
81 });
82 });
83
84 let parsed = Memo::new(move |_| overrides_json.with(|raw| parse_overrides(raw)));
85 let override_error = Signal::derive(move || parsed.with(|(_, error)| error.clone()));
86
87 view! {
88 <div class="admin-runner">
89 <AdminPageHead
90 title="代码运行器"
91 actions=|| view! { <StatusDot label="Docker 沙箱" tone="success"/> }
92 >
93 "在沙箱里试运行代码,确认文章里可运行代码块的预期输出。"
94 </AdminPageHead>
95 <div class="runner-admin-grid">
96 <section class="runner-admin-main" aria-label="编辑与输出">
97 <SegmentedControl
98 class="runner-admin-langs"
99 aria_label="语言"
100 items={
101 SUPPORTED_LANGS
102 .iter()
103 .map(|(v, l)| SegmentedItem::new(v.to_string(), *l))
104 .collect::<Vec<_>>()
105 }
106 value=lang
107 on_change=move |l: String| {
108 if l != lang.get_untracked() {
109 source.set(default_source(&l));
110 lang.set(l);
111 }
112 }
113 />
114 {move || {
117 let language = lang.get();
118 let overrides = parsed.with_untracked(|(o, _)| o.clone());
119 view! {
120 <div class="runner-admin-runner">
121 <CodeRunner
122 source=source.get_untracked()
123 language=language
124 overrides=overrides
125 instance_id=0
126 />
127 </div>
128 }
129 }}
130 </section>
131 <aside class="admin-panel runner-admin-side" aria-label="运行配置">
132 <h2>"资源上限"</h2>
133 {move || match limits.get() {
134 Some(c) => {
135 view! {
136 <dl>
137 <dt>"超时"</dt>
138 <dd>{format!("{} 秒", c.max_timeout_secs)}</dd>
139 <dt>"内存"</dt>
140 <dd>{format!("{} MB", c.max_memory_mb)}</dd>
141 <dt>"CPU"</dt>
142 <dd>{format!("{:.1} 核", c.max_cpu_cores)}</dd>
143 <dt>"网络"</dt>
144 <dd>{if c.allow_network { "开启" } else { "关闭" }}</dd>
145 </dl>
146 }
147 .into_any()
148 }
149 None => view! { <p class="runner-admin-hint">"正在读取…"</p> }.into_any(),
150 }}
151 <div>
152 <FormLabel
153 label="临时覆盖(JSON,可选)"
154 html_for="runner-overrides".to_string()
155 />
156 <FormTextarea
157 id="runner-overrides"
158 class="field field-textarea settings-mono"
159 rows=4
160 placeholder=OVERRIDES_PLACEHOLDER
161 invalid=Signal::derive(move || !override_error.with(String::is_empty))
162 value=overrides_json
163 oninput=move |v: String| overrides_json.set(v)
164 />
165 {move || {
166 if override_error.with(String::is_empty) {
167 view! {
168 <p class="runner-admin-hint">
169 "可覆盖 cpu_cores、memory_mb、timeout_secs、output_bytes、allow_network,仍受上限约束。"
170 </p>
171 }
172 .into_any()
173 } else {
174 view! {
175 <p class="runner-admin-hint" data-error="true" role="alert">
176 {move || override_error.get()}
177 </p>
178 }
179 .into_any()
180 }
181 }}
182 </div>
183 <a class="runner-admin-link" href=Route::SiteSettingsPage {}.to_string()>
184 "在设置里调整上限"
185 </a>
186 </aside>
187 </div>
188 </div>
189 }
190}