Skip to content

Commit dfdc561

Browse files
authored
Merge pull request #151 from rambit-systems/push-ompzuvxrwwvq
Incremental UI Push #31
2 parents 09fc3eb + fb5d6b8 commit dfdc561

File tree

7 files changed

+254
-23
lines changed

7 files changed

+254
-23
lines changed

crates/site-app/src/components.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod copy_button;
12
mod create_button;
23
mod data_table;
34
pub mod form_layout;
@@ -10,6 +11,7 @@ mod refetch_while_focused;
1011
mod store_path;
1112

1213
pub use self::{
13-
create_button::*, data_table::*, icons::*, input_field::*, item_links::*,
14-
navbar::*, popover::*, refetch_while_focused::*, store_path::*,
14+
copy_button::*, create_button::*, data_table::*, icons::*, input_field::*,
15+
item_links::*, navbar::*, popover::*, refetch_while_focused::*,
16+
store_path::*,
1517
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use leptos::prelude::*;
2+
3+
use crate::components::DocumentDuplicateHeroIcon;
4+
5+
#[island]
6+
pub fn CopyButton(copy_content: String) -> impl IntoView {
7+
let copy_content = Signal::stored(copy_content);
8+
9+
let on_click = move |_| {
10+
#[cfg(feature = "hydrate")]
11+
(leptos_use::use_clipboard().copy)(&copy_content())
12+
};
13+
14+
const CLASS: &str = "cursor-pointer stroke-[2.0] stroke-base-11/50 \
15+
hover:stroke-base-11/75 transition-colors";
16+
17+
view! {
18+
<DocumentDuplicateHeroIcon on:click={on_click} {..} class=CLASS />
19+
}
20+
}

crates/site-app/src/components/icons.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ pub fn ChevronDownHeroIcon() -> impl IntoView {
4747
}
4848
}
4949

50+
#[component]
51+
pub fn DocumentDuplicateHeroIcon() -> impl IntoView {
52+
view! {
53+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
54+
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
55+
</svg>
56+
}
57+
}
58+
5059
#[component]
5160
pub fn EnvelopeHeroIcon() -> impl IntoView {
5261
view! {

crates/site-app/src/hooks/entry_hook.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use crate::resources::entry::entry_query_scope;
66

77
#[derive(Clone)]
88
pub struct EntryHook {
9-
_key: Callback<(), RecordId<Entry>>,
10-
resource: Resource<Result<Option<Entry>, ServerFnError>>,
9+
_key: Callback<(), RecordId<Entry>>,
10+
self_resource: Resource<Result<Option<Entry>, ServerFnError>>,
1111
}
1212

1313
impl EntryHook {
@@ -16,17 +16,19 @@ impl EntryHook {
1616
key: impl Fn() -> RecordId<Entry> + Copy + Send + Sync + 'static,
1717
) -> Self {
1818
let client = expect_context::<QueryClient>();
19-
let resource = client.resource(entry_query_scope(), key);
19+
let self_resource = client.resource(entry_query_scope(), key);
2020

2121
Self {
2222
_key: Callback::new(move |_| key()),
23-
resource,
23+
self_resource,
2424
}
2525
}
2626

27-
pub fn all(&self) -> AsyncDerived<Result<Option<Entry>, ServerFnError>> {
27+
pub fn intrensic(
28+
&self,
29+
) -> AsyncDerived<Result<Option<Entry>, ServerFnError>> {
2830
AsyncDerived::new({
29-
let resource = self.resource;
31+
let resource = self.self_resource;
3032
move || async move { resource.await }
3133
})
3234
}

crates/site-app/src/pages/entry.rs

Lines changed: 199 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
use leptos::prelude::*;
2+
use leptos_fetch::QueryClient;
23
use leptos_router::hooks::use_params_map;
3-
use models::{dvf::RecordId, Entry};
4+
use models::{
5+
dvf::{RecordId, Visibility},
6+
Cache, Entry, PvCache, StorePath,
7+
};
48

5-
use crate::{hooks::EntryHook, pages::UnauthorizedPage};
9+
use crate::{
10+
components::{CacheItemLink, CopyButton, LockClosedHeroIcon},
11+
hooks::EntryHook,
12+
pages::UnauthorizedPage,
13+
};
614

715
#[component]
816
pub fn EntryPage() -> impl IntoView {
@@ -13,25 +21,201 @@ pub fn EntryPage() -> impl IntoView {
1321
.parse::<RecordId<_>>()
1422
.ok();
1523

16-
requested_entry
17-
.map(|e| view! { <EntryTile id=e /> }.into_any())
18-
.unwrap_or(view! { <UnauthorizedPage /> }.into_any())
24+
let Some(requested_entry) = requested_entry else {
25+
return view! { <UnauthorizedPage /> }.into_any();
26+
};
27+
28+
let entry_hook = EntryHook::new(move || requested_entry);
29+
let intrensic = entry_hook.intrensic();
30+
let intrensic_suspend = move || {
31+
Suspend::new(async move {
32+
match intrensic.await {
33+
Ok(Some(entry)) => view! { <EntryInner entry=entry /> }.into_any(),
34+
Ok(None) => view! { <MissingEntryPage /> }.into_any(),
35+
Err(_) => view! { <ErrorEntryPage /> }.into_any(),
36+
}
37+
})
38+
};
39+
40+
view! {
41+
<Suspense fallback=|| ()>{ intrensic_suspend }</Suspense>
42+
}
43+
.into_any()
44+
}
45+
46+
#[component]
47+
fn EntryInner(entry: Entry) -> impl IntoView {
48+
view! {
49+
<div class="flex flex-col gap-4">
50+
<TitleTile store_path={entry.store_path.clone()} />
51+
<div class="grid gap-4 grid-flow-col">
52+
<StorePathTile store_path={entry.store_path.clone()} />
53+
<CachesTile entry={entry.clone()} />
54+
</div>
55+
</div>
56+
}
57+
}
58+
59+
#[component]
60+
fn TitleTile(store_path: StorePath<String>) -> impl IntoView {
61+
let path = store_path.to_absolute_path();
62+
view! {
63+
<div class="p-6 elevation-flat flex flex-row gap-2 items-center">
64+
<p class="text-base-12 text-xl">
65+
{ path.clone() }
66+
</p>
67+
<CopyButton copy_content={path} {..} class="size-6" />
68+
</div>
69+
}
70+
}
71+
72+
#[component]
73+
fn StorePathTile(store_path: StorePath<String>) -> impl IntoView {
74+
let string = store_path.to_string();
75+
let separator_index =
76+
string.find('-').expect("no separator found in store path");
77+
let (digest, _) = string.split_at(separator_index);
78+
let name = store_path.name().clone();
79+
80+
const KEY_CLASS: &str = "place-self-end";
81+
const VALUE_CLASS: &str = "text-base-12 font-medium";
82+
83+
let value_element = move |s: &str| {
84+
view! {
85+
<div class="flex flex-row gap-2 items-center">
86+
<p class=VALUE_CLASS>{ s }</p>
87+
<CopyButton
88+
copy_content={s.to_string()}
89+
{..} class="size-4"
90+
/>
91+
</div>
92+
}
93+
};
94+
95+
view! {
96+
<div class="p-6 elevation-flat flex flex-col gap-4">
97+
<p class="subtitle">
98+
"Store Path Breakdown"
99+
</p>
100+
<div class="grid gap-x-4 gap-y-1 grid-cols-[repeat(2,auto)]">
101+
<p class=KEY_CLASS>"Prefix"</p>
102+
{ value_element("/nix/store/") }
103+
<p class=KEY_CLASS>"Digest"</p>
104+
{ value_element(digest) }
105+
<p class=KEY_CLASS>"Name"</p>
106+
{ value_element(&name) }
107+
</div>
108+
</div>
109+
}
19110
}
20111

21112
#[component]
22-
fn EntryTile(id: RecordId<Entry>) -> impl IntoView {
23-
let entry_hook = EntryHook::new(move || id);
24-
let all = entry_hook.all();
25-
let all_suspend =
26-
move || Suspend::new(async move { format!("{:#?}", all.await) });
113+
fn CachesTile(entry: Entry) -> impl IntoView {
114+
let caches = Signal::stored(entry.caches.clone());
115+
let store_path = Signal::stored(entry.store_path);
116+
view! {
117+
<div class="p-6 elevation-flat flex flex-col gap-4">
118+
<p class="subtitle">
119+
"Resident Caches"
120+
</p>
121+
122+
<table class="table">
123+
<thead>
124+
<th>"Name"</th>
125+
<th>"Download Url"</th>
126+
<th>"Visibility"</th>
127+
</thead>
128+
<tbody class="animate-fade-in min-h-10">
129+
<For each=caches key=|r| *r children=move |r| view! {
130+
<CachesTileRow store_path=store_path cache_id=r />
131+
} />
132+
</tbody>
133+
</table>
134+
</div>
135+
}
136+
}
137+
138+
#[component]
139+
fn CachesTileRow(
140+
store_path: Signal<StorePath<String>>,
141+
cache_id: RecordId<Cache>,
142+
) -> impl IntoView {
143+
let query_client = expect_context::<QueryClient>();
144+
145+
let query_scope = crate::resources::cache::cache_query_scope();
146+
let resource = query_client.resource(query_scope, move || cache_id);
147+
148+
let suspend = move || {
149+
Suspend::new(async move {
150+
match resource.await {
151+
Ok(Some(c)) => {
152+
view! { <CachesTileDataRow cache=c store_path=store_path /> }
153+
.into_any()
154+
}
155+
Ok(None) => None::<()>.into_any(),
156+
Err(e) => format!("Error: {e}").into_any(),
157+
}
158+
})
159+
};
27160

28161
view! {
29-
<div class="elevation-flat p-4 flex flex-col gap-4">
30-
<p class="title">"Entry"</p>
162+
<Transition fallback=|| ()>{ suspend }</Transition>
163+
}
164+
}
165+
166+
#[component]
167+
fn CachesTileDataRow(
168+
store_path: Signal<StorePath<String>>,
169+
cache: PvCache,
170+
) -> impl IntoView {
171+
let download_url = format!(
172+
"/api/v1/c/{cache_name}/download/{store_path}",
173+
cache_name = cache.name,
174+
store_path = store_path(),
175+
);
176+
let vis_icon =
177+
matches!(cache.visibility, Visibility::Private).then_some(view! {
178+
<LockClosedHeroIcon {..} class="size-4 stroke-base-11/75 stroke-[2.0]" />
179+
});
31180

32-
<div class="bg-base-2 rounded border-[1.5px] border-base-6 p-4 overflow-x-auto"><pre>
33-
<Suspense fallback=|| ()>{ all_suspend }</Suspense>
34-
</pre></div>
181+
view! {
182+
<tr>
183+
<th scope="row">
184+
<CacheItemLink id=cache.id extra_class="text-link-primary"/>
185+
</th>
186+
<td>
187+
<a href={download_url} class="text-link text-link-primary">"Download"</a>
188+
</td>
189+
<td class="flex flex-row items-center gap-1">
190+
{ cache.visibility.to_string() }
191+
{ vis_icon }
192+
</td>
193+
</tr>
194+
}
195+
}
196+
197+
#[component]
198+
fn MissingEntryPage() -> impl IntoView {
199+
view! {
200+
<div class="p-6 elevation-flat flex flex-col gap-4 items-center">
201+
<p class="title">
202+
"( ˶°ㅁ°) !!"
203+
" We don't have that entry"
204+
</p>
205+
<p>"Looks like we don't have that entry! Try uploading it through the CLI."</p>
206+
</div>
207+
}
208+
}
209+
210+
#[component]
211+
fn ErrorEntryPage() -> impl IntoView {
212+
view! {
213+
<div class="p-6 elevation-flat flex flex-col gap-4 items-center">
214+
<p class="title">
215+
"ヽ(°〇°)ノ"
216+
" Something went wrong"
217+
</p>
218+
<p>"Looks like something went wrong when finding your entry. We apologize!"</p>
35219
</div>
36220
}
37221
}

crates/site-app/style/src/main.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@
5050
--form-grid-cols: calc(48 * var(--spacing)) minmax(0, 1fr)
5151
}
5252

53+
:root {
54+
font-size: 85%;
55+
}
56+
57+
@media (min-width: 40rem) {
58+
:root {
59+
font-size: inherit;
60+
}
61+
}
62+
5363
@utility grid-cols-form {
5464
grid-template-columns: var(--form-grid-cols);
5565
}

crates/site-app/style/src/typography.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@
33
.title {
44
@apply font-display text-3xl font-semibold tracking-tight text-base-12;
55
}
6+
7+
.subtitle {
8+
@apply text-xl font-semibold tracking-tight text-base-12;
9+
}
610
}

0 commit comments

Comments
 (0)