Skip to content

Commit

Permalink
feat: added snapshot_list and single view UI
Browse files Browse the repository at this point in the history
  • Loading branch information
sauraww committed Sep 26, 2024
1 parent 6594ee1 commit 7bf8df3
Show file tree
Hide file tree
Showing 7 changed files with 366 additions and 1 deletion.
42 changes: 41 additions & 1 deletion crates/frontend/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use leptos::ServerFnError;
use serde_json::Value;

use crate::{
types::{
Config, DefaultConfig, Dimension, ExperimentResponse, ExperimentsResponse,
FetchTypeTemplateResponse, FunctionResponse, ListFilters,
FetchTypeTemplateResponse, FunctionResponse, ListFilters, SnapshotResponse,
},
utils::{
construct_request_headers, get_host, parse_json_response, request,
Expand Down Expand Up @@ -51,6 +52,24 @@ pub async fn fetch_default_config(
Ok(response)
}

pub async fn get_snapshots(tenant: String) -> Result<SnapshotResponse, ServerFnError> {
let client = reqwest::Client::new();
let host = use_host_server();

let url = format!("{host}/config-versions");
let response: SnapshotResponse = client
.get(url)
.header("x-tenant", tenant)
.send()
.await
.map_err(|e| ServerFnError::new(e.to_string()))?
.json()
.await
.map_err(|e| ServerFnError::new(e.to_string()))?;

Ok(response)
}

pub async fn delete_context(
tenant: String,
context_id: String,
Expand Down Expand Up @@ -248,3 +267,24 @@ pub async fn fetch_types(
.await
.map_err(err_handler)
}

pub async fn get_config_by_version(
tenant: String,
version: String,
) -> Result<Value, ServerFnError> {
let client = reqwest::Client::new();
let host = use_host_server();

let url = format!("{host}/config?version={}", version);
let response: Value = client
.get(url)
.header("x-tenant", tenant)
.send()
.await
.map_err(|e| ServerFnError::new(e.to_string()))?
.json()
.await
.map_err(|e| ServerFnError::new(e.to_string()))?;

Ok(response)
}
25 changes: 25 additions & 0 deletions crates/frontend/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use crate::pages::experiment_list::ExperimentList;
use crate::pages::function::{
function_create::CreateFunctionView, function_list::FunctionList, FunctionPage,
};
use crate::pages::snapshot_list::SnapshotList;
use crate::pages::snapshotconfig::SnapshotConfig;
use crate::pages::{
context_override::ContextOverride, custom_types::TypesPage,
default_config::DefaultConfig, experiment::ExperimentPage, home::Home,
Expand Down Expand Up @@ -211,6 +213,29 @@ pub fn app(app_envs: Envs) -> impl IntoView {
}
/>

<Route
ssr=SsrMode::Async
path="/admin/:tenant/snapshots"
view=move || {
view! {
<Layout>
<SnapshotList/>
</Layout>
}
}
/>
<Route
ssr=SsrMode::Async
path="/admin/:tenant/snapshots/:version"
view=move || {
view! {
<Layout>
<SnapshotConfig/>
</Layout>
}
}
/>

// <Route
// path="/*any"
// view=move || {
Expand Down
6 changes: 6 additions & 0 deletions crates/frontend/src/components/side_nav.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ fn create_routes(tenant: &str) -> Vec<AppRoute> {
icon: "ri-t-box-fill".to_string(),
label: "Type Templates".to_string(),
},
AppRoute {
key: format!("{base}/admin/{tenant}/snapshots"),
path: format!("{base}/admin/{tenant}/snapshots"),
icon: "ri-camera-lens-fill".to_string(),
label: "Snapshots".to_string(),
},
]
}

Expand Down
2 changes: 2 additions & 0 deletions crates/frontend/src/pages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ pub mod experiment_list;
pub mod function;
pub mod home;
pub mod not_found;
pub mod snapshot_list;
pub mod snapshotconfig;
219 changes: 219 additions & 0 deletions crates/frontend/src/pages/snapshot_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
use leptos::*;

use chrono::{DateTime, TimeZone, Utc};
use leptos_router::A;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};

use crate::components::skeleton::Skeleton;
use crate::components::stat::Stat;
use crate::components::table::types::TablePaginationProps;
use crate::components::table::{types::Column, Table};
use crate::types::{ListFilters, Snapshot};

use crate::api::get_snapshots;

#[derive(Serialize, Deserialize, Clone, Debug)]
struct CombinedResource {
snapshots: Vec<Snapshot>,
}

#[component]
pub fn snapshot_list() -> impl IntoView {
let tenant_rs = use_context::<ReadSignal<String>>().unwrap();

// Signals for filters
let (filters, set_filters) = create_signal(ListFilters {
status: None,
from_date: Utc.timestamp_opt(0, 0).single(),
to_date: Utc.timestamp_opt(4130561031, 0).single(),
page: Some(1),
count: Some(10), // Limit of 10 items per page
});

let table_columns =
create_memo(move |_| snapshot_table_columns(tenant_rs.get_untracked()));

let combined_resource: Resource<String, CombinedResource> = create_blocking_resource(
move || tenant_rs.get(),
|current_tenant| async move {
match get_snapshots(current_tenant.to_string()).await {
Ok(response) => CombinedResource {
snapshots: response.data,
},
Err(_) => CombinedResource { snapshots: vec![] },
}
},
);

let handle_next_click = Callback::new(move |total_pages: i64| {
set_filters.update(|f| {
f.page = match f.page {
Some(p) if p < total_pages => Some(p + 1),
Some(p) => Some(p),
None => Some(1),
}
});
});

let handle_prev_click = Callback::new(move |_| {
set_filters.update(|f| {
f.page = match f.page {
Some(p) if p > 1 => Some(p - 1),
Some(_) => Some(1),
None => Some(1),
}
});
});

view! {
<div class="p-8">
<Suspense fallback=move || view! { <Skeleton/> }>
<div class="pb-4">
{move || {
let snapshot_res = combined_resource.get();
let total_items = match snapshot_res {
Some(snapshot_resp) => snapshot_resp.snapshots.len().to_string(),
_ => "0".to_string(),
};
view! {
<Stat heading="Snapshots" icon="ri-camera-lens-fill" number=total_items/>
}
}}
</div>
<div class="card rounded-xl w-full bg-base-100 shadow">
<div class="card-body">
<div class="flex justify-between">
<h2 class="card-title">Snapshots</h2>
</div>
<div>
{move || {
let value = combined_resource.get();
let filters = filters.get();
match value {
Some(v) => {
// Pagination calculations
let page = filters.page.unwrap_or(1);
let count = filters.count.unwrap_or(10);
let total_items = v.snapshots.len();
let total_pages = (total_items as f64 / count as f64).ceil() as i64;

// Get the items for the current page
let start = ((page - 1) * count as i64) as usize;
let end = std::cmp::min(start + count as usize, total_items);

let data = v
.snapshots[start..end]
.iter()
.map(|snapshot| {
let mut snapshot_map = json!(snapshot)
.as_object()
.unwrap()
.to_owned();

if let Some(id_value) = snapshot_map.get("id") {
let id_string = match id_value {
Value::Number(num) => num.to_string(),
Value::String(s) => s.clone(),
_ => "".to_string(),
};
snapshot_map.insert("id".to_string(), Value::String(id_string));
}

// Format the created_at field
if let Some(created_at_str) = snapshot_map.get("created_at").and_then(|v| v.as_str()) {
if let Ok(created_at_dt) = DateTime::parse_from_rfc3339(created_at_str) {
snapshot_map.insert(
"created_at".to_string(),
json!(created_at_dt.format("%v").to_string()),
);
}
}

snapshot_map
})
.collect::<Vec<Map<String, Value>>>()
.to_owned();

let pagination_props = TablePaginationProps {
enabled: true,
count,
current_page: page,
total_pages,
on_next: handle_next_click,
on_prev: handle_prev_click,
};

view! {
<Table
cell_class="".to_string()
rows=data
key_column="id".to_string()
columns=table_columns.get()
pagination=pagination_props
/>
}
}
None => view! { <div>Loading....</div> }.into_view(),
}
}}
</div>
</div>
</div>
</Suspense>
</div>
}
}

pub fn snapshot_table_columns(tenant: String) -> Vec<Column> {
vec![
Column::new(
"id".to_string(),
None,
|value: &str, _row: &Map<String, Value>| {
let id = value.to_string();
view! {
<div>{id}</div>
}
.into_view()
},
),
Column::new(
"config".to_string(),
None,
move |_value: &str, row: &Map<String, Value>| {
let id = row.get("id").and_then(|v| v.as_str()).unwrap_or_default();
let href = format!("/admin/{}/snapshots/{}", tenant, id);
view! {
<div>
<A href=href class="btn-link">
"View Config"
</A>
</div>
}
.into_view()
},
),
Column::default("config_hash".to_string()),
Column::default("created_at".to_string()),
Column::new(
"tags".to_string(),
None,
|_value: &str, row: &Map<String, Value>| {
let tags = row.get("tags").and_then(|v| v.as_array());
match tags {
Some(arr) => {
let tags_str = arr
.iter()
.map(|v| v.as_str().unwrap_or(""))
.collect::<Vec<&str>>()
.join(", ");
view! { <span>{tags_str}</span> }
}
None => view! { <span>"-"</span> },
}
.into_view()
},
),
]
}
57 changes: 57 additions & 0 deletions crates/frontend/src/pages/snapshotconfig.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use crate::api::get_config_by_version;
use leptos::*;
use leptos_router::use_params_map;

#[component]
pub fn SnapshotConfig() -> impl IntoView {
let params = use_params_map();
let tenant = params.with(|p| p.get("tenant").cloned().unwrap_or_default());
let version = params.with(|p| p.get("version").cloned().unwrap_or_default());

view! {
<div class="p-8">
{
let config_resource = create_blocking_resource(
move || (tenant.clone(), version.clone()),
|(tenant, version)| async move {
get_config_by_version(tenant, version).await
},
);

view! {
<Suspense fallback=move || view! { <p>"Loading..."</p> }.into_view()>
{move || {
match config_resource.get() {
Some(Ok(config)) => {
// Display the config JSON
let config_json = serde_json::to_string_pretty(&config).unwrap_or_default();
view! {
<div>
<andypf-json-viewer
indent="4"
expanded="true"
theme="default-light"
show-data-types="false"
show-toolbar="true"
expand-icon-type="arrow"
expanded="1"
show-copy="true"
show-size="false"
data=config_json
></andypf-json-viewer>

</div>
}
.into_view()
}
Some(Err(_)) => view! { <div> <pre>"Error loading config."</pre></div> }.into_view(),
None => view! { <div> <pre>"Loading..."</pre></div> }.into_view(),
}
}}
</Suspense>
}
.into_view()
}
</div>
}
}
Loading

0 comments on commit 7bf8df3

Please sign in to comment.