1use crate::locks::Lock;
2use crate::namespace::Namespace;
3
4pub struct Overview {
5 pub namespace: Namespace,
6 pub objects: u64,
7 pub bytes: u64,
8 pub locks: Vec<Lock>,
9 pub writable: bool,
10}
11
12pub fn render(overview: &Overview) -> String {
13 let Overview {
14 namespace,
15 objects,
16 bytes,
17 locks,
18 writable,
19 } = overview;
20
21 format!(
22 r#"<!doctype html>
23<html lang="en">
24<head>
25<meta charset="utf-8">
26<meta name="viewport" content="width=device-width, initial-scale=1">
27<title>{namespace} — LFSX</title>
28<style>{STYLE}</style>
29</head>
30<body>
31<main>
32<h1>{namespace}</h1>
33<dl>
34<div><dt>Objects</dt><dd>{objects}</dd></div>
35<div><dt>On disk</dt><dd>{}</dd></div>
36<div><dt>Your access</dt><dd>{}</dd></div>
37</dl>
38<h2>Locks</h2>
39{}
40<footer>Read only. Objects are reclaimed with <code>lfsx gc</code>, locks are released with
41<code>git lfs unlock</code>.</footer>
42</main>
43</body>
44</html>
45"#,
46 human_bytes(*bytes),
47 if *writable { "read and write" } else { "read" },
48 locks_table(locks),
49 )
50}
51
52fn locks_table(locks: &[Lock]) -> String {
53 if locks.is_empty() {
54 return "<p class=\"empty\">Nothing is locked.</p>".to_owned();
55 }
56
57 let rows: String = locks
58 .iter()
59 .map(|lock| {
60 format!(
61 "<tr><td>{}</td><td>{}</td><td>{}</td></tr>",
62 escape(&lock.path),
63 escape(&lock.owner.name),
64 escape(&lock.locked_at)
65 )
66 })
67 .collect();
68
69 format!(
70 "<table><thead><tr><th>Path</th><th>Held by</th><th>Since</th></tr></thead><tbody>{rows}</tbody></table>"
71 )
72}
73
74fn human_bytes(bytes: u64) -> String {
75 const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
76 let mut size = bytes as f64;
77 let mut unit = 0;
78
79 while size >= 1024.0 && unit < UNITS.len() - 1 {
80 size /= 1024.0;
81 unit += 1;
82 }
83
84 if unit == 0 {
85 format!("{bytes} B")
86 } else {
87 format!("{size:.1} {}", UNITS[unit])
88 }
89}
90
91fn escape(raw: &str) -> String {
92 raw.replace('&', "&")
93 .replace('<', "<")
94 .replace('>', ">")
95 .replace('"', """)
96}
97
98const STYLE: &str = "\
99:root{color-scheme:light dark}\
100body{font:16px/1.5 system-ui,sans-serif;margin:0;padding:2rem}\
101main{max-width:52rem;margin:0 auto}\
102h1{font-size:1.5rem;margin:0 0 1.5rem}\
103h2{font-size:1.1rem;margin:2rem 0 .75rem}\
104dl{display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:1rem;margin:0}\
105dt{font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;opacity:.65}\
106dd{margin:.25rem 0 0;font-size:1.5rem;font-variant-numeric:tabular-nums}\
107table{width:100%;border-collapse:collapse}\
108th{text-align:left;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;opacity:.65;font-weight:400}\
109th,td{padding:.5rem 0;border-bottom:1px solid color-mix(in srgb,currentColor 12%,transparent)}\
110td{font-variant-numeric:tabular-nums}\
111.empty{opacity:.65}\
112footer{margin-top:2.5rem;font-size:.85rem;opacity:.65}\
113code{font-family:ui-monospace,monospace;font-size:.85em}";
114
115#[cfg(test)]
116mod tests;