Skip to main content

krabka_admin_ui/views/
page.rs

1use super::Route;
2use crate::{
3    dto::{AclRow, GroupRow, LogDirRow, QuotaRow, TopicRow, UserRow},
4    permissions::Capabilities,
5};
6
7#[derive(Clone, PartialEq, Eq)]
8pub enum ReadRouteState<T> {
9    Loading,
10    AuthenticationRequired,
11    /// The session is valid and the operator's ACLs do not permit the page.
12    NotPermitted,
13    LoadFailed,
14    Rows(Vec<T>),
15}
16
17#[derive(Clone, Copy, PartialEq, Eq)]
18pub enum LoginRouteState {
19    Form,
20    AuthenticationFailed,
21    /// No broker answered, so the UI cannot say whether the credentials are
22    /// good.
23    BrokerUnavailable,
24}
25
26/// What the shell knows about the operator it renders for.
27///
28/// An anonymous view carries no CSRF token, so it renders no mutation form:
29/// nothing can prove that a signed-in operator asked for the mutation.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct OperatorView {
32    capabilities: Capabilities,
33    csrf_token: Option<String>,
34}
35
36impl OperatorView {
37    #[must_use]
38    pub const fn new(capabilities: Capabilities, csrf_token: String) -> Self {
39        Self {
40            capabilities,
41            csrf_token: Some(csrf_token),
42        }
43    }
44
45    /// The view for a page that no session stands behind.
46    #[must_use]
47    pub const fn anonymous() -> Self {
48        Self {
49            capabilities: Capabilities::all(),
50            csrf_token: None,
51        }
52    }
53
54    #[must_use]
55    pub const fn capabilities(&self) -> Capabilities {
56        self.capabilities
57    }
58
59    #[must_use]
60    pub fn csrf_token(&self) -> Option<&str> {
61        self.csrf_token.as_deref()
62    }
63}
64
65impl Default for OperatorView {
66    fn default() -> Self {
67        Self::anonymous()
68    }
69}
70
71#[derive(Clone, PartialEq, Eq)]
72pub enum RoutePage {
73    Overview {
74        message: Option<&'static str>,
75    },
76    Login {
77        state: LoginRouteState,
78    },
79    Topics {
80        state: ReadRouteState<TopicRow>,
81    },
82    Groups {
83        state: ReadRouteState<GroupRow>,
84    },
85    Acls {
86        state: ReadRouteState<AclRow>,
87    },
88    Users {
89        state: ReadRouteState<UserRow>,
90    },
91    Quotas {
92        state: ReadRouteState<QuotaRow>,
93        /// The entity the rows belong to, when the operator asked for one
94        /// other than their own account.
95        entity: Option<String>,
96    },
97    LogDirs {
98        state: ReadRouteState<LogDirRow>,
99    },
100}
101
102impl RoutePage {
103    #[must_use]
104    pub const fn overview() -> Self {
105        Self::Overview { message: None }
106    }
107
108    #[must_use]
109    pub const fn signed_in() -> Self {
110        Self::Overview {
111            message: Some("Signed in."),
112        }
113    }
114
115    #[must_use]
116    pub const fn login() -> Self {
117        Self::Login {
118            state: LoginRouteState::Form,
119        }
120    }
121
122    #[must_use]
123    pub const fn login_failed() -> Self {
124        Self::Login {
125            state: LoginRouteState::AuthenticationFailed,
126        }
127    }
128
129    #[must_use]
130    pub const fn login_broker_unavailable() -> Self {
131        Self::Login {
132            state: LoginRouteState::BrokerUnavailable,
133        }
134    }
135
136    #[must_use]
137    pub const fn for_unauthenticated_route(route: Route) -> Self {
138        Self::for_guarded_route(route.guard_for_authentication(false))
139    }
140
141    #[must_use]
142    pub const fn for_authenticated_route(route: Route) -> Self {
143        Self::for_guarded_route(route.guard_for_authentication(true))
144    }
145
146    const fn for_guarded_route(route: Route) -> Self {
147        match route {
148            Route::Overview => Self::overview(),
149            Route::Login => Self::login(),
150            Route::Topics => Self::topics(ReadRouteState::Loading),
151            Route::Groups => Self::groups(ReadRouteState::Loading),
152            Route::Acls => Self::acls(ReadRouteState::Loading),
153            Route::Users => Self::users(ReadRouteState::Loading),
154            Route::Quotas => Self::quotas(ReadRouteState::Loading),
155            Route::LogDirs => Self::log_dirs(ReadRouteState::Loading),
156        }
157    }
158
159    #[must_use]
160    pub const fn topics(state: ReadRouteState<TopicRow>) -> Self {
161        Self::Topics { state }
162    }
163
164    #[must_use]
165    pub const fn groups(state: ReadRouteState<GroupRow>) -> Self {
166        Self::Groups { state }
167    }
168
169    #[must_use]
170    pub const fn acls(state: ReadRouteState<AclRow>) -> Self {
171        Self::Acls { state }
172    }
173
174    #[must_use]
175    pub const fn users(state: ReadRouteState<UserRow>) -> Self {
176        Self::Users { state }
177    }
178
179    #[must_use]
180    pub const fn quotas(state: ReadRouteState<QuotaRow>) -> Self {
181        Self::Quotas {
182            state,
183            entity: None,
184        }
185    }
186
187    #[must_use]
188    pub const fn quotas_for_entity(
189        state: ReadRouteState<QuotaRow>,
190        entity: Option<String>,
191    ) -> Self {
192        Self::Quotas { state, entity }
193    }
194
195    #[must_use]
196    pub const fn log_dirs(state: ReadRouteState<LogDirRow>) -> Self {
197        Self::LogDirs { state }
198    }
199
200    const fn title(&self) -> &'static str {
201        match self {
202            Self::Overview { .. } => "Krabka Admin",
203            Self::Login { .. } => "Sign in to Krabka",
204            Self::Topics { .. } => "Topics",
205            Self::Groups { .. } => "Consumer Groups",
206            Self::Acls { .. } => "ACLs",
207            Self::Users { .. } => "SCRAM Users",
208            Self::Quotas { .. } => "Quotas",
209            Self::LogDirs { .. } => "Log Dirs",
210        }
211    }
212}
213
214#[must_use]
215pub fn render_page(page: &RoutePage) -> String {
216    render_page_for_operator(page, &OperatorView::anonymous())
217}
218
219#[must_use]
220pub fn render_page_for_operator(page: &RoutePage, operator: &OperatorView) -> String {
221    let mut html = String::new();
222    html.push_str(
223        r#"<!doctype html>
224<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>"#,
225    );
226    push_escaped(&mut html, page.title());
227    html.push_str("</title></head><body>");
228    html.push_str(&render_page_body_html_for_operator(page, operator));
229    html.push_str("</body></html>");
230    html
231}
232
233#[must_use]
234pub fn render_page_body_html(page: &RoutePage) -> String {
235    render_page_body_html_for_operator(page, &OperatorView::anonymous())
236}
237
238#[must_use]
239pub fn render_page_body_html_for_operator(page: &RoutePage, operator: &OperatorView) -> String {
240    dioxus_ssr::render_element(super::render_page_element_for_operator(page, operator))
241}
242
243fn push_escaped(html: &mut String, value: &str) {
244    for character in value.chars() {
245        match character {
246            '&' => html.push_str("&amp;"),
247            '<' => html.push_str("&lt;"),
248            '>' => html.push_str("&gt;"),
249            '"' => html.push_str("&quot;"),
250            '\'' => html.push_str("&#39;"),
251            _ => html.push(character),
252        }
253    }
254}