Skip to main content

tablo_core/panel/build/
handle.rs

1//! [`PanelHandle`]: a mounted panel kept for a job, a test or a public page.
2
3use std::sync::Arc;
4
5use toasty::Db;
6use topcoat::{
7    Result,
8    context::Cx,
9    view::{BoxView, View, ViewExt},
10};
11
12use super::validation_cx;
13use crate::{
14    bus::NotificationBus,
15    notification::Notifications,
16    panel::{
17        Panel,
18        state::{PanelState, current, enter},
19    },
20    resource::Mounts,
21};
22
23/// A panel's mounted resources and its database, which a background job or a test keeps to build
24/// a context per run without declaring the panel again. A router's mounted panel gives it through
25/// [`panel_handle`](super::RouterBuilderPanelExt::panel_handle), a panel no router mounts through
26/// [`Panel::handle`]. Cloning it is cheap.
27///
28/// ```rust,no_run
29/// # use tablo_core::{NoForm, Panel, Resource, RouterBuilderPanelExt, Tenant, scoped_query};
30/// # use topcoat::router::{Router, RouterBuilderDiscoverExt};
31/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
32/// # struct Post { #[key] #[auto] id: uuid::Uuid, title: String }
33/// # struct PostResource;
34/// # impl Resource for PostResource { type Model = Post; type Form = NoForm<Post>; }
35/// # async fn run(db: toasty::Db, tenant: uuid::Uuid) -> topcoat::Result<()> {
36/// let builder = Router::builder()
37///     .discover()
38///     .app_context(db)
39///     .panel(Panel::new("admin").resource::<PostResource>())?;
40/// let admin = builder.panel_handle("admin").expect("mounted above");
41/// let router = builder.build();
42///
43/// // In the job, once per run:
44/// let cx = admin.context().with(Tenant(tenant));
45/// let posts = scoped_query::<PostResource>(&cx)?;
46/// # let _ = (router, posts);
47/// # Ok(())
48/// # }
49/// ```
50#[derive(Clone)]
51pub struct PanelHandle {
52    pub(super) db: Db,
53    pub(super) mounts: Arc<Mounts>,
54    /// The mounted panel, for a handle a router gives; `None` for [`Panel::handle`]'s.
55    pub(super) panel: Option<Arc<PanelState>>,
56    pub(super) prefix: String,
57    /// The router's bus, or one no bell listens on for [`Panel::handle`]'s.
58    pub(super) bus: NotificationBus,
59}
60
61impl PanelHandle {
62    /// A context outside any request in which the panel's resources answer as the panel mounted
63    /// them: what a job or a test passes to [`scoped_query`](crate::scoped_query),
64    /// [`can`](crate::can), [`write_create`](crate::write_create) and the other entry points that
65    /// answer from a mounted def. It holds the database and the resources, and no request,
66    /// session or tenant. Add a tenant with `cx.with(Tenant(id))`. It carries no signed-in user,
67    /// so a policy that reads one answers as for a signed-out request.
68    ///
69    /// The context is one unit of work, as a request is: loads it memoizes stay cached for its
70    /// lifetime, so a job builds one per run. A context built with no panel at all knows no
71    /// resource: every entry point that reads a mounted def answers as for a resource the panel
72    /// does not mount.
73    pub fn context(&self) -> Cx {
74        validation_cx(&self.db, &self.mounts)
75    }
76
77    /// `cx` answering as this panel, for a request outside every panel's prefix, such as a
78    /// public page: [`scoped_query`](crate::scoped_query), [`can`](crate::can) and the other
79    /// entry points that answer from a mounted def answer as this panel mounted its resources,
80    /// and, for a handle a router gives, the [`url`](crate::url) helpers answer with its URLs.
81    /// A request outside every prefix answers as no panel until it enters one, however many
82    /// panels the router mounts. It runs no auth gate and signs no one in, so a public page's
83    /// context knows no user. Enter only outside every prefix: under a panel's prefix, a handle
84    /// [`Panel::handle`] gives leaves the request answering as its own panel.
85    ///
86    /// ```rust,no_run
87    /// # use tablo_core::{NoForm, PanelHandle, Resource, scoped_query};
88    /// # use topcoat::{Result, context::Cx};
89    /// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
90    /// # struct Post { #[key] #[auto] id: uuid::Uuid, title: String }
91    /// # struct PostResource;
92    /// # impl Resource for PostResource { type Model = Post; type Form = NoForm<Post>; }
93    /// async fn public_posts(cx: &Cx, admin: &PanelHandle) -> Result<Vec<Post>> {
94    ///     let cx = admin.enter(cx);
95    ///     Ok(scoped_query::<PostResource>(&cx)?
96    ///         .exec(&mut tablo_core::db::db(&cx)?)
97    ///         .await?)
98    /// }
99    /// ```
100    pub fn enter(&self, cx: &Cx) -> Cx {
101        match &self.panel {
102            Some(panel) => enter(cx, panel),
103            None => cx.with(EnteredPanel(Arc::clone(&self.mounts))),
104        }
105    }
106
107    /// The panel's prefix, e.g. `/admin`, for a public page's link to it; `None` for
108    /// [`Panel::handle`]'s, which no router mounts.
109    pub fn url(&self) -> Option<String> {
110        self.panel.as_ref().map(|panel| panel.realm.prefix.clone())
111    }
112
113    /// Sends stored notifications to this panel's users from outside a request, such as from a
114    /// job, for no tenant until [`Notifications::tenant`] names one. The bells they have open
115    /// update when the handle is a router's.
116    ///
117    /// ```rust,no_run
118    /// # use tablo_core::{Notification, PanelHandle};
119    /// # async fn nightly(admin: &PanelHandle, owner_id: &str, tenant: uuid::Uuid) -> topcoat::Result<()> {
120    /// admin
121    ///     .notifications()
122    ///     .tenant(tenant)
123    ///     .send(owner_id, Notification::info("The nightly import finished"))
124    ///     .await?;
125    /// # Ok(())
126    /// # }
127    /// ```
128    pub fn notifications(&self) -> Notifications {
129        Notifications::new(self.db.clone(), self.bus.clone(), self.prefix.clone(), None)
130    }
131
132    /// Renders a complete HTML document around a page outside every prefix with this panel's
133    /// shell settings: its stylesheet, font, scripts, favicon and dark-mode default. A handle
134    /// [`Panel::handle`] gives renders the document without them.
135    ///
136    /// ```rust,no_run
137    /// # use tablo_core::PanelHandle;
138    /// # use topcoat::{Result, context::{Cx, app_context}, router::{Slot, layout}, view::{View, view}};
139    /// /// The handle `panel_handle("admin")` returned, kept in the app context.
140    /// struct Admin(PanelHandle);
141    ///
142    /// #[layout("/blog")]
143    /// async fn blog_layout(cx: &Cx, slot: Slot<'_>) -> Result<impl View> {
144    ///     let Admin(admin) = app_context::<Admin>(cx);
145    ///     admin.document(cx, "Blog", view! { <main>(slot)</main> }).await
146    /// }
147    /// ```
148    pub async fn document<'a>(
149        &'a self,
150        cx: &'a Cx,
151        title: impl Into<String>,
152        body: impl View + 'a,
153    ) -> Result<BoxView<'a>> {
154        Panel::render_document(cx, self.panel.as_deref(), title.into(), body.boxed()).await
155    }
156}
157
158/// The resources [`PanelHandle::enter`] put in scope for a request outside every panel.
159pub(super) struct EnteredPanel(Arc<Mounts>);
160
161/// The resources the request answers from: its own panel's, or the ones
162/// [`PanelHandle::enter`] put in scope.
163pub(super) fn request_mounts(cx: &Cx) -> Option<&Mounts> {
164    match current(cx) {
165        Some(panel) => Some(&*panel.mounts),
166        None => topcoat::context::try_request_context::<EnteredPanel>(cx)
167            .map(|EnteredPanel(mounts)| &**mounts),
168    }
169}