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}