yggdrasil/pages/admin/settings/
runner_section.rs1use dioxus::prelude::*;
6
7#[cfg(target_arch = "wasm32")]
8use crate::api::settings::{get_runner_settings, update_runner_settings};
9#[cfg(target_arch = "wasm32")]
10use crate::components::forms::{FormLabel, INPUT_CLASS};
11#[cfg(target_arch = "wasm32")]
12use crate::components::ui::{Checkbox, LoadingButton, ADMIN_CARD_CLASS};
13#[cfg(target_arch = "wasm32")]
14use crate::models::settings::RunnerSettings;
15
16#[allow(non_snake_case)]
18#[component]
19pub fn RunnerSection(toast: Callback<(String, bool)>) -> Element {
20 #[cfg(target_arch = "wasm32")]
21 {
22 let mut saved: Signal<RunnerSettings> = use_signal(RunnerSettings::default);
23 let mut draft: Signal<RunnerSettings> = use_signal(RunnerSettings::default);
24 let loading = use_signal(|| true);
25 let mut saving = use_signal(|| false);
26 let mut just_saved = use_signal(|| false);
27
28 use_effect(move || {
29 #[cfg(target_arch = "wasm32")]
30 {
31 let mut saved = saved;
32 let mut draft = draft;
33 let mut loading = loading;
34 spawn(async move {
35 match get_runner_settings().await {
36 Ok(s) => {
37 saved.set(s.clone());
38 draft.set(s);
39 }
40 Err(e) => toast.call((format!("加载失败:{e}"), true)),
41 }
42 loading.set(false);
43 });
44 }
45 });
46
47 let dirty = use_memo(move || draft() != saved());
48
49 rsx! {
50 div { class: "space-y-6",
51 div { class: "{ADMIN_CARD_CLASS} p-6 md:p-8 flex flex-col gap-6",
52 div { class: "flex items-center gap-3",
53 span { class: "inline-flex items-center justify-center w-10 h-10 rounded-full bg-[var(--color-paper-theme)] text-[var(--color-paper-primary)] border border-[var(--color-paper-border)]",
54 svg {
55 class: "w-5 h-5",
56 view_box: "0 0 24 24",
57 fill: "none",
58 stroke: "currentColor",
59 stroke_width: "1.8",
60 stroke_linecap: "round",
61 stroke_linejoin: "round",
62 path { d: "M16 18l6-6-6-6 M8 6l-6 6 6 6" }
63 }
64 }
65 div {
66 h2 { class: "text-xl font-bold text-[var(--color-paper-primary)]",
67 "代码运行器"
68 }
69 p { class: "text-sm text-[var(--color-paper-secondary)] mt-0.5",
70 span { class: "text-amber-600 dark:text-amber-400 font-medium",
71 "⚠ 修改后需重启服务生效。"
72 }
73 }
74 }
75 }
76
77 label { class: "flex items-center gap-3 cursor-pointer max-w-xl",
78 Checkbox {
79 checked: draft().allow_network,
80 onchange: move |checked: bool| {
81 let mut d = draft();
82 d.allow_network = checked;
83 draft.set(d);
84 just_saved.set(false);
85 },
86 }
87 div { class: "flex flex-col",
88 span { class: "text-sm font-medium text-[var(--color-paper-primary)]",
89 "允许容器联网"
90 }
91 span { class: "text-xs text-[var(--color-paper-secondary)]",
92 "⚠️ 启用后用户代码可访问网络,存在安全风险。"
93 }
94 }
95 }
96
97 NumberField {
98 id: "runner-concurrent".to_string(),
99 label: "最大并发任务数".to_string(),
100 value: draft().max_concurrent.to_string(),
101 oninput: move |v: String| {
102 if let Ok(n) = v.parse::<u32>() {
103 let mut d = draft();
104 d.max_concurrent = n;
105 draft.set(d);
106 just_saved.set(false);
107 }
108 },
109 }
110 NumberField {
111 id: "runner-cpu".to_string(),
112 label: "每任务最大 CPU 核数".to_string(),
113 value: format!("{:.1}", draft().max_cpu_cores),
114 oninput: move |v: String| {
115 if let Ok(n) = v.parse::<f64>() {
116 let mut d = draft();
117 d.max_cpu_cores = n;
118 draft.set(d);
119 just_saved.set(false);
120 }
121 },
122 is_float: true,
123 }
124 NumberField {
125 id: "runner-mem".to_string(),
126 label: "每任务最大内存(MB)".to_string(),
127 value: draft().max_memory_mb.to_string(),
128 oninput: move |v: String| {
129 if let Ok(n) = v.parse::<u32>() {
130 let mut d = draft();
131 d.max_memory_mb = n;
132 draft.set(d);
133 just_saved.set(false);
134 }
135 },
136 }
137 NumberField {
138 id: "runner-timeout".to_string(),
139 label: "每任务最大执行超时(秒)".to_string(),
140 value: draft().max_timeout_secs.to_string(),
141 oninput: move |v: String| {
142 if let Ok(n) = v.parse::<u32>() {
143 let mut d = draft();
144 d.max_timeout_secs = n;
145 draft.set(d);
146 just_saved.set(false);
147 }
148 },
149 }
150 NumberField {
151 id: "runner-output".to_string(),
152 label: "每任务最大输出字节数".to_string(),
153 value: draft().max_output_bytes.to_string(),
154 oninput: move |v: String| {
155 if let Ok(n) = v.parse::<u64>() {
156 let mut d = draft();
157 d.max_output_bytes = n;
158 draft.set(d);
159 just_saved.set(false);
160 }
161 },
162 }
163 NumberField {
164 id: "runner-source".to_string(),
165 label: "每任务最大源码字节数".to_string(),
166 value: draft().max_source_bytes.to_string(),
167 oninput: move |v: String| {
168 if let Ok(n) = v.parse::<u64>() {
169 let mut d = draft();
170 d.max_source_bytes = n;
171 draft.set(d);
172 just_saved.set(false);
173 }
174 },
175 }
176 NumberField {
177 id: "runner-queue".to_string(),
178 label: "排队等待超时(秒)".to_string(),
179 value: draft().queue_timeout_secs.to_string(),
180 oninput: move |v: String| {
181 if let Ok(n) = v.parse::<u32>() {
182 let mut d = draft();
183 d.queue_timeout_secs = n;
184 draft.set(d);
185 just_saved.set(false);
186 }
187 },
188 }
189 NumberField {
190 id: "runner-ttl".to_string(),
191 label: "历史 task 保留时长(秒)".to_string(),
192 value: draft().task_ttl_secs.to_string(),
193 oninput: move |v: String| {
194 if let Ok(n) = v.parse::<u32>() {
195 let mut d = draft();
196 d.task_ttl_secs = n;
197 draft.set(d);
198 just_saved.set(false);
199 }
200 },
201 }
202
203 div { class: "flex flex-col gap-2 max-w-xl",
204 FormLabel {
205 label: "语言白名单(逗号分隔,留空=全部)",
206 html_for: Some("runner-langs".to_string()),
207 }
208 input {
209 id: "runner-langs",
210 r#type: "text",
211 class: "{INPUT_CLASS}",
212 value: draft().languages.as_deref().unwrap_or(""),
213 placeholder: "python,node,rust(留空=全部)",
214 oninput: move |e: Event<FormData>| {
215 let v = e.value();
216 let mut d = draft();
217 d.languages = if v.trim().is_empty() { None } else { Some(v) };
218 draft.set(d);
219 just_saved.set(false);
220 },
221 }
222 p { class: "text-xs text-[var(--color-paper-secondary)]",
223 "限制可用语言。留空表示全部注册语言可用。"
224 }
225 }
226
227 div { class: "flex items-center justify-between gap-4 pt-1",
228 if just_saved() {
229 span { class: "inline-flex items-center gap-1.5 text-xs text-[var(--color-paper-accent)]",
230 svg {
231 class: "w-3.5 h-3.5",
232 view_box: "0 0 24 24",
233 fill: "none",
234 stroke: "currentColor",
235 stroke_width: "2.5",
236 path {
237 stroke_linecap: "round",
238 stroke_linejoin: "round",
239 d: "M5 13l4 4L19 7",
240 }
241 }
242 "已保存(重启后生效)"
243 }
244 } else if dirty() {
245 span { class: "text-xs text-[var(--color-paper-secondary)]",
246 "有未保存的更改"
247 }
248 } else {
249 span { class: "text-xs text-transparent select-none", "·" }
250 }
251 LoadingButton {
252 label: "保存设置".to_string(),
253 loading: saving(),
254 disabled: loading() || just_saved() || !dirty(),
255 onclick: move |_| {
256 let d = draft();
257 saving.set(true);
258 spawn(async move {
259 match update_runner_settings(
260 d.allow_network,
261 d.max_concurrent,
262 d.max_cpu_cores,
263 d.max_memory_mb,
264 d.max_timeout_secs,
265 d.max_output_bytes,
266 d.max_source_bytes,
267 d.queue_timeout_secs,
268 d.task_ttl_secs,
269 d.languages,
270 )
271 .await
272 {
273 Ok(s) => {
274 saved.set(s.clone());
275 draft.set(s);
276 just_saved.set(true);
277 toast.call(("保存成功".to_string(), false));
278 }
279 Err(e) => toast.call((format!("保存失败:{e}"), true)),
280 }
281 saving.set(false);
282 });
283 },
284 }
285 }
286 }
287 }
288 }
289 }
290 #[cfg(not(target_arch = "wasm32"))]
291 {
292 rsx! {
293 div { class: "p-8 text-[var(--color-paper-secondary)]", "运行器配置(前端渲染)" }
294 }
295 }
296}
297#[cfg(target_arch = "wasm32")]
298#[allow(non_snake_case)]
300#[component]
301fn NumberField(
302 id: String,
303 label: String,
304 value: String,
305 oninput: Callback<String>,
306 #[props(default = false)] is_float: bool,
307) -> Element {
308 rsx! {
309 div { class: "flex flex-col gap-2 max-w-xl",
310 FormLabel { label: "{label}", html_for: Some(id.clone()) }
311 input {
312 id: "{id}",
313 r#type: "number",
314 step: if is_float { "0.1" } else { "1" },
315 class: "{INPUT_CLASS}",
316 value: "{value}",
317 oninput: move |e: Event<FormData>| oninput.call(e.value()),
318 }
319 }
320 }
321}