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 NotPermitted,
13 LoadFailed,
14 Rows(Vec<T>),
15}
16
17#[derive(Clone, Copy, PartialEq, Eq)]
18pub enum LoginRouteState {
19 Form,
20 AuthenticationFailed,
21 BrokerUnavailable,
24}
25
26#[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 #[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 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("&"),
247 '<' => html.push_str("<"),
248 '>' => html.push_str(">"),
249 '"' => html.push_str("""),
250 '\'' => html.push_str("'"),
251 _ => html.push(character),
252 }
253 }
254}