Skip to content

Commit 44b53cd

Browse files
committed
fix: graceful 504 UI
cors change is justified because it's a public API with no auth
1 parent cb788d7 commit 44b53cd

3 files changed

Lines changed: 73 additions & 59 deletions

File tree

frontend/src/app.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::components::editor::Editor;
2-
use crate::components::output::OutputContainer;
2+
use crate::components::output::{CompileTimer, OutputContainer};
33
use crate::utils::query::Query;
44
use crate::{ActionButtonState, ActionButtonStateContext, icon};
55
use gloo::history::{BrowserHistory, History};
@@ -110,7 +110,9 @@ pub fn App() -> Html {
110110
</div>
111111
<div class="w-full min-h-0">
112112
if let Some(ref data) = *data {
113-
<OutputContainer value={data} key={*run_count} />
113+
<Suspense fallback={html! { <CompileTimer /> }}>
114+
<OutputContainer value={data} key={*run_count} />
115+
</Suspense>
114116
}
115117
</div>
116118
</Split>

frontend/src/components/output.rs

Lines changed: 65 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,92 @@
11
use crate::api::BACKEND_URL;
22
use crate::{ActionButtonState, ActionButtonStateContext};
33
use gloo::timers::callback::Interval;
4-
use gloo_net::http::QueryParams;
4+
use gloo_net::http::{QueryParams, Request};
55
use std::cell::Cell;
66
use std::rc::Rc;
7+
use yew::HtmlResult;
78
use yew::prelude::*;
9+
use yew::suspense::use_future_with;
10+
11+
#[derive(Clone, PartialEq)]
12+
enum CompileResult {
13+
Done(String),
14+
ColdStartTimeout,
15+
}
816

917
#[derive(Properties, PartialEq, Eq)]
1018
pub struct OutputContainerProps {
1119
pub value: Rc<str>,
1220
}
1321

1422
#[component]
15-
pub fn OutputContainer(props: &OutputContainerProps) -> Html {
23+
pub fn OutputContainer(props: &OutputContainerProps) -> HtmlResult {
1624
let action_button_state = use_context::<ActionButtonStateContext>().unwrap();
17-
let loading = use_state(|| true);
18-
let elapsed = use_state(|| 0u32);
19-
let src = use_state(String::new);
2025

21-
{
22-
let loading = loading.clone();
23-
let src = src.clone();
24-
let elapsed = elapsed.clone();
25-
use_effect_with(Rc::clone(&props.value), move |value| {
26-
elapsed.set(0);
27-
loading.set(true);
28-
src.set({
29-
let query = QueryParams::new();
30-
query.append("code", value);
31-
format!("{}/run?{}", BACKEND_URL, query)
32-
});
33-
})
34-
};
26+
let result = use_future_with(Rc::clone(&props.value), |value| async move {
27+
let query = QueryParams::new();
28+
query.append("code", &value);
29+
let url = format!("{}/run?{}", BACKEND_URL, query);
30+
31+
match Request::get(&url).send().await {
32+
Ok(resp) if resp.status() == 504 => CompileResult::ColdStartTimeout,
33+
Ok(resp) => CompileResult::Done(resp.text().await.unwrap_or_default()),
34+
Err(_) => CompileResult::ColdStartTimeout,
35+
}
36+
})?;
3537

3638
{
37-
let elapsed = elapsed.clone();
38-
let is_loading = *loading;
39-
use_effect_with(is_loading, move |is_loading| {
40-
let _interval = is_loading.then(|| {
41-
let counter = Rc::new(Cell::new(0u32));
42-
Interval::new(1_000, {
43-
let counter = counter.clone();
44-
let elapsed = elapsed.clone();
45-
move || {
46-
let next = counter.get() + 1;
47-
counter.set(next);
48-
elapsed.set(next);
49-
}
50-
})
51-
});
52-
move || drop(_interval)
39+
let action_button_state = action_button_state.clone();
40+
use_effect(move || {
41+
action_button_state.dispatch(ActionButtonState::Enabled);
5342
});
5443
}
5544

56-
let onload = {
57-
let loading = loading.clone();
58-
move |_| {
59-
loading.set(false);
60-
action_button_state.dispatch(ActionButtonState::Enabled);
45+
Ok(match &*result {
46+
CompileResult::Done(html_content) => {
47+
html! { <iframe srcdoc={html_content.clone()} class="w-full h-full" /> }
6148
}
62-
};
63-
64-
let classes = classes!(
65-
"w-full",
66-
"h-full",
67-
if *loading { "invisible" } else { "visible" }
68-
);
69-
70-
html! {
71-
<>
72-
if *loading {
49+
CompileResult::ColdStartTimeout => {
50+
html! {
7351
<div class="h-full bg-gray-600 flex items-center justify-center">
74-
<div class="text-gray-200 text-lg flex flex-col items-center gap-2">
75-
<span class="animate-spin inline-block w-8 h-8 border-[3px] border-gray-200 border-t-transparent rounded-full"></span>
76-
<span>{format!("waiting for the backend service... {}s", *elapsed)}</span>
52+
<div class="text-gray-200 flex flex-col items-center gap-3 max-w-md text-center">
53+
<span class="text-xl font-semibold">{"Backend service is waking up"}</span>
54+
<span class="text-gray-400">{"The service was asleep and timed out on the first request. It should be ready now."}</span>
55+
<span class="text-gray-300">{"Hit Run again."}</span>
7756
</div>
7857
</div>
7958
}
80-
<iframe src={AttrValue::from((*src).clone())} {onload} class={classes} />
81-
</>
59+
}
60+
})
61+
}
62+
63+
#[component]
64+
pub fn CompileTimer() -> Html {
65+
let elapsed = use_state(|| 0u32);
66+
67+
{
68+
let elapsed = elapsed.clone();
69+
use_effect_with((), move |_| {
70+
let counter = Rc::new(Cell::new(0u32));
71+
let interval = Interval::new(1_000, {
72+
let counter = counter.clone();
73+
let elapsed = elapsed.clone();
74+
move || {
75+
let next = counter.get() + 1;
76+
counter.set(next);
77+
elapsed.set(next);
78+
}
79+
});
80+
move || drop(interval)
81+
});
82+
}
83+
84+
html! {
85+
<div class="h-full bg-gray-600 flex items-center justify-center">
86+
<div class="text-gray-200 text-lg flex flex-col items-center gap-2">
87+
<span class="animate-spin inline-block w-8 h-8 border-[3px] border-gray-200 border-t-transparent rounded-full"></span>
88+
<span>{format!("waiting for the backend service... {}s", *elapsed)}</span>
89+
</div>
90+
</div>
8291
}
8392
}

services/backend/src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use tokio::net::TcpListener;
1313
use tokio::process::Command;
1414
use tower::ServiceBuilder;
1515
use tower::limit::GlobalConcurrencyLimitLayer;
16+
use tower_http::cors::CorsLayer;
1617
use tower_http::trace::TraceLayer;
1718
use tracing::{debug, error, info};
1819

@@ -165,7 +166,9 @@ async fn main() {
165166
.layer(GlobalConcurrencyLimitLayer::new(1))
166167
.layer(TraceLayer::new_for_http());
167168

168-
let app = Router::new().nest("/api", api);
169+
let app = Router::new()
170+
.nest("/api", api)
171+
.layer(CorsLayer::permissive());
169172

170173
let addr = format!("0.0.0.0:{}", *PORT);
171174
let listener = TcpListener::bind(&addr).await.unwrap();

0 commit comments

Comments
 (0)