use std::{future::Future, pin::Pin, sync::Arc};
use http_body_util::BodyExt;
use topcoat::{
Result,
context::Cx,
router::{
Body, Layer, LayerFuture, Next, Path, PathBuf,
error::{content_too_large, forbidden},
},
runtime::shard,
view::{BoxView, View, view},
};
use super::{
build::route_path,
gate::gate,
list::{load_table_page, table_error_view, wire_table_actions},
state::{CurrentPanel, current, panels},
};
use crate::{
policy::Ability,
resource::Resource,
table::{TableSignals, TableState},
};
pub(crate) type SearchFn =
for<'a> fn(
&'a Cx,
String,
TableSignals,
) -> Pin<Box<dyn Future<Output = Result<BoxView<'a>>> + Send + 'a>>;
pub(crate) struct RelationRequest {
pub(crate) seed: String,
pub(crate) page: String,
pub(crate) read_only: bool,
}
pub(crate) type RelationSearchFn = Arc<
dyn for<'a> Fn(
&'a Cx,
RelationRequest,
TableSignals,
) -> Pin<Box<dyn Future<Output = Result<BoxView<'a>>> + Send + 'a>>
+ Send
+ Sync,
>;
pub(crate) fn list_search<R: Resource>(
cx: &Cx,
path: String,
signals: TableSignals,
) -> Pin<Box<dyn Future<Output = Result<BoxView<'_>>> + Send + '_>> {
Box::pin(async move {
let resource = gate::<R>(cx)?;
if !resource.can(cx, Ability::ViewAny) {
return Err(forbidden().into());
}
let table = wire_table_actions(cx, &resource, true);
let mut state = TableState::from_query(&signals.query.get());
state.delete = None;
state.open = None;
let state = table.normalize_state(&state);
let retry_signals = signals.clone();
let rendered = async {
let page = load_table_page(cx, &resource, &table, &state).await?;
table.render_live(cx, page, &state, &path, signals).await
};
match rendered.await {
Ok(view) => Ok(view),
Err(error) => Ok(table_error_view(
cx,
&resource,
&state,
&error,
&path,
Some(&retry_signals),
)),
}
})
}
fn search_entry(cx: &Cx, path: &str) -> Result<SearchFn> {
crate::auth::guard(cx)?;
current(cx)
.and_then(|panel| panel.search.get(path).copied())
.ok_or_else(|| topcoat::router::error::not_found().into())
}
#[shard("/_topcoat/runtime/shards/tablo-table-search")]
pub(crate) async fn table_search(
cx: &Cx,
path: String,
query: topcoat::runtime::Signal<String>,
bulk: topcoat::runtime::Signal<String>,
) -> Result<impl View> {
let entry = search_entry(cx, &path)?;
entry(cx, path, TableSignals { query, bulk }).await
}
pub(crate) fn list_search_invocation<'a>(
cx: &'a Cx,
path: &str,
signals: TableSignals,
) -> impl View + use<'a> {
let path = path.to_string();
let TableSignals { query, bulk } = signals;
view! { cx => table_search(path: $(path.clone()), query: $(query), bulk: $(bulk)) }
}
pub(crate) const TABLE_SEARCH_PATH: &str = "/_topcoat/runtime/shards/tablo-table-search";
fn relation_entry(cx: &Cx, parent: &str, child: &str) -> Result<RelationSearchFn> {
crate::auth::guard(cx)?;
current(cx)
.and_then(|panel| {
panel
.relations
.get(&(parent.to_string(), child.to_string()))
.cloned()
})
.ok_or_else(|| topcoat::router::error::not_found().into())
}
#[shard("/_topcoat/runtime/shards/tablo-table-relation-search")]
pub(crate) async fn table_relation_search(
cx: &Cx,
scope: String,
page: String,
read_only: bool,
query: topcoat::runtime::Signal<String>,
bulk: topcoat::runtime::Signal<String>,
) -> Result<impl View> {
let mut parts = scope.splitn(3, '/');
let (parent, child, seed) = (
parts.next().unwrap_or_default(),
parts.next().unwrap_or_default(),
parts.next().unwrap_or_default(),
);
let entry = relation_entry(cx, parent, child)?;
entry(
cx,
RelationRequest {
seed: seed.to_string(),
page,
read_only,
},
TableSignals { query, bulk },
)
.await
}
pub(crate) fn relation_search_invocation<'a>(
cx: &'a Cx,
parent: &str,
child: &str,
request: RelationRequest,
signals: TableSignals,
) -> impl View + use<'a> {
let RelationRequest {
seed,
page,
read_only,
} = request;
let scope = format!("{parent}/{child}/{seed}");
let TableSignals { query, bulk } = signals;
view! {
cx =>
table_relation_search(
scope: $(scope.clone()),
page: $(page.clone()),
read_only: $(read_only),
query: $(query),
bulk: $(bulk)
)
}
}
pub(crate) const TABLE_RELATION_SEARCH_PATH: &str =
"/_topcoat/runtime/shards/tablo-table-relation-search";
const MAX_SHARD_BYTES: usize = 64 * 1024;
pub(crate) struct ShardPanel {
path: PathBuf,
arg: usize,
}
impl ShardPanel {
pub(crate) fn new(path: &str, arg: usize) -> Self {
Self {
path: route_path(path),
arg,
}
}
}
#[derive(serde::Deserialize)]
struct ShardArgs {
args: Vec<serde_json::Value>,
}
impl Layer for ShardPanel {
fn path(&self) -> Option<&Path> {
Some(&self.path)
}
fn handle<'a>(&'a self, cx: &'a Cx, body: Body, next: Next<'a>) -> LayerFuture<'a> {
Box::pin(async move {
let bytes = http_body_util::Limited::new(body, MAX_SHARD_BYTES)
.collect()
.await
.map_err(|_| content_too_large())?
.to_bytes();
let panel = serde_json::from_slice::<ShardArgs>(&bytes)
.ok()
.and_then(|request| request.args.get(self.arg)?.as_str().map(str::to_owned))
.and_then(|path: String| panels(cx)?.by_path(&path).cloned());
let body = Body::from(bytes);
match panel {
Some(panel) => next.run(&cx.with(CurrentPanel(panel)), body).await,
None => next.run(cx, body).await,
}
})
}
}
#[cfg(test)]
mod tests;