1mod actions;
4mod build;
5mod detail;
6mod forms;
7pub(crate) mod gate;
8mod headers;
9mod list;
10mod pages;
11mod register;
12mod relations;
13mod shell;
14pub(crate) mod state;
15#[cfg(test)]
16pub(crate) mod test_support;
17pub mod url;
18mod write;
19
20use std::path::PathBuf;
21
22use topcoat::{asset::Asset, font::Font, router::LayoutRenderFn};
23
24pub use self::{build::RouterBuilderPanelExt, gate::can_list, shell::Brand};
25use self::{
26 build::is_directory_pattern,
27 register::{PageRegistration, Registration, ResourceRegistration},
28 shell::ShellAssets,
29};
30pub(crate) use self::{build::route_path, forms::parse_form_body, gate::panel_prefix};
31use crate::{
32 DeclarationError, DeclarationErrorKind, Page,
33 resource::{Resource, ResourceDef},
34};
35
36pub fn wired_table<R: Resource>(
44 cx: &topcoat::context::Cx,
45) -> topcoat::Result<crate::table::WiredTable<R::Model>> {
46 let resource = crate::resource::require_mounted::<R>(cx)?;
47 Ok(self::list::wire_table_actions(cx, &resource))
48}
49
50pub fn navigation<R: Resource>(cx: &topcoat::context::Cx) -> Option<crate::NavigationItem> {
53 crate::resource::mounted::<R>(cx).map(|resource| resource.navigation.clone())
54}
55
56pub struct Panel {
73 prefix: String,
74 shell_assets: Option<ShellAssets>,
75 brand: Option<Brand>,
76 dark_mode: Option<bool>,
77 layout: Option<LayoutRenderFn>,
79 registrations: Vec<Box<dyn Registration>>,
81 frame_ancestors: Option<String>,
84 configuration_errors: Vec<DeclarationError>,
86 uploads: Option<crate::upload::InstalledUploader>,
88 served_dirs: Vec<(String, PathBuf)>,
90 login_hint: Option<String>,
91 auth: crate::auth::Auth,
92}
93
94impl Panel {
95 pub fn new(prefix: impl Into<String>) -> Self {
97 let raw = prefix.into();
98 let trimmed = raw.trim().trim_matches('/').to_string();
99 let prefix = if trimmed.is_empty() {
100 "/admin".to_string()
101 } else {
102 format!("/{trimmed}")
103 };
104 let configuration_errors = prefix
105 .trim_matches('/')
106 .split('/')
107 .filter_map(|segment| build::validate_route_segment("panel prefix", segment).err())
108 .map(DeclarationError::panel)
109 .collect();
110 Self {
111 prefix,
112 shell_assets: None,
113 brand: None,
114 dark_mode: None,
115 layout: None,
116 registrations: Vec::new(),
117 frame_ancestors: Some(headers::DEFAULT_FRAME_ANCESTORS.to_string()),
118 configuration_errors,
119 uploads: None,
120 served_dirs: Vec::new(),
121 login_hint: None,
122 auth: crate::auth::Auth::default(),
123 }
124 }
125
126 pub fn prefix(&self) -> &str {
128 &self.prefix
129 }
130
131 pub fn uploads(mut self, uploader: impl crate::Uploader) -> Self {
134 self.uploads = Some(crate::upload::InstalledUploader::new(uploader));
135 self
136 }
137
138 pub fn serve_dir(mut self, path: impl Into<String>, dir: impl Into<PathBuf>) -> Self {
141 let path = path.into();
142 if !is_directory_pattern(&path) {
143 self.configuration_errors.push(DeclarationError::panel(
144 DeclarationErrorKind::ServeDirWithoutCatchAll { path: path.clone() },
145 ));
146 }
147 self.served_dirs.push((path, dir.into()));
148 self
149 }
150
151 pub fn shell_assets(mut self, stylesheet: Asset, font: Font) -> Self {
154 self.shell_assets = Some(ShellAssets { stylesheet, font });
155 self
156 }
157
158 pub fn resource<R: Resource>(self) -> Self {
161 self.resource_with::<R>(|def| def)
162 }
163
164 pub fn resource_with<R: Resource>(
171 mut self,
172 customize: impl FnOnce(ResourceDef<R>) -> ResourceDef<R> + Send + 'static,
173 ) -> Self {
174 self.registrations
175 .push(Box::new(ResourceRegistration::<R>(Box::new(customize))));
176 self
177 }
178
179 pub fn page<P: Page>(mut self) -> Self {
182 self.registrations.push(Box::new(PageRegistration::<P> {
183 home: false,
184 _page: std::marker::PhantomData,
185 }));
186 self
187 }
188
189 pub fn home<P: Page>(mut self) -> Self {
192 self.registrations.push(Box::new(PageRegistration::<P> {
193 home: true,
194 _page: std::marker::PhantomData,
195 }));
196 self
197 }
198
199 pub fn layout(mut self, render: LayoutRenderFn) -> Self {
202 self.layout = Some(render);
203 self
204 }
205
206 pub fn brand(mut self, brand: Brand) -> Self {
208 self.brand = Some(brand);
209 self
210 }
211
212 pub fn frame_ancestors(mut self, ancestors: impl Into<String>) -> Self {
215 self.frame_ancestors = Some(ancestors.into());
216 self
217 }
218
219 pub fn without_frame_ancestors(mut self) -> Self {
221 self.frame_ancestors = None;
222 self
223 }
224
225 pub fn dark_mode(mut self, enabled: bool) -> Self {
227 self.dark_mode = Some(enabled);
228 self
229 }
230
231 pub fn auth(mut self, auth: crate::auth::Auth) -> Self {
234 self.auth = auth;
235 self
236 }
237
238 pub fn login_hint(mut self, hint: impl Into<String>) -> Self {
240 self.login_hint = Some(hint.into());
241 self
242 }
243}
244
245enum Root {
247 Redirect(String),
249 Home,
251}
252
253#[cfg(test)]
254mod tests;