feat: implement new data display component
This commit is contained in:
1 parent
07c1679e1a
commit
eae8d2a26a
6 files changed
+506
-355
No files matched your search
+205
-217
@@ -1,265 +1,253 @@
|
||||
use crate::common::{SchemaNode, SchemaType, ValueType};
|
||||
use indexmap::IndexMap;
|
||||
use leptos::logging::debug_log;
|
||||
use crate::common::{SchemaContainerStyle, SchemaHeaderStyle, SchemaNode, SchemaType, ValueType};
|
||||
use leptos::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// # Value Display Component
|
||||
/// This component displays any data from a given `HashMap<String, ValueType>`. It requires a schema
|
||||
/// to tell it how to display the information, which is covered in the [`SchemaNode`] documentation.
|
||||
#[component]
|
||||
pub fn ValueDisplay(
|
||||
#[prop(into)] schema: Signal<SchemaNode>,
|
||||
/// The starting member. Starts display from this member in the `data_map`, if it exists.
|
||||
pub fn DataDisplay(
|
||||
/// The schema that this component should use to display the given data.
|
||||
#[prop(into)]
|
||||
schema: Signal<SchemaNode>,
|
||||
/// The starting key in the data map.
|
||||
#[prop(into, optional)]
|
||||
data_member: Signal<String>,
|
||||
#[prop(into)] data_map: Signal<HashMap<String, ValueType>>,
|
||||
/// A `HashMap` containing all the data that should be displayed.
|
||||
#[prop(into)]
|
||||
data_map: Signal<HashMap<String, ValueType>>,
|
||||
) -> impl IntoView {
|
||||
let data_value = Memo::new(move |_| {
|
||||
if data_member.get().is_empty() {
|
||||
ValueType::Map(Some(data_map.get()))
|
||||
// The schema should contain information about the header type and styling, along with the
|
||||
// information display. This is rendered as part of the DataDisplayWrapper, which is simply a
|
||||
// wrapper around the information to be displayed.
|
||||
|
||||
// The children are all SchemaNodes as well, meaning that this component needs to call itself
|
||||
// for each of these children. They will of course contain all the same information that we
|
||||
// expect here.
|
||||
|
||||
// The actual displayed information is controlled through a DataDisplayView, which will accept
|
||||
// settings for how the information is parsed. For example, rendering a list may be done by
|
||||
// a string join operation, or it may be a list of maps, each of which needs to be displayed with
|
||||
// a specific set of children. The SchemaNode must be able to handle this, and these options
|
||||
// should be given to the DataDisplayView somehow.
|
||||
|
||||
// As the display_value here is actually an Option<ValueType>, we want to be able to handle cases
|
||||
// where the display is given no values. In this case, the SchemaNode needs to specify whether
|
||||
// a missing value is expected or an error.
|
||||
|
||||
let display_value = Memo::new(move |_| {
|
||||
let member = data_member.get();
|
||||
if member.is_empty() {
|
||||
Some(ValueType::Map(Some(data_map.get())))
|
||||
} else {
|
||||
data_map
|
||||
.get()
|
||||
.get(&data_member.get())
|
||||
.map(|v| v.clone())
|
||||
.unwrap_or_default()
|
||||
data_map.get().get(&member).cloned()
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
{move || {
|
||||
match schema.get().data_type {
|
||||
SchemaType::String | SchemaType::Number => {
|
||||
view! {
|
||||
<PrimitiveDisplay
|
||||
header=move || schema.get().header
|
||||
value=data_value
|
||||
nested=true
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
SchemaType::List => {
|
||||
view! {
|
||||
<ListDisplay
|
||||
header=move || schema.get().header
|
||||
value=data_value
|
||||
item_template=move || {
|
||||
schema
|
||||
.get()
|
||||
.item_template
|
||||
.unwrap_or(Box::from(SchemaNode::new(SchemaType::List)))
|
||||
}
|
||||
nested=true
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
SchemaType::Map => {
|
||||
view! {
|
||||
<MapDisplay
|
||||
value=data_value
|
||||
header=move || schema.get().header
|
||||
schema=move || schema.get().children
|
||||
nested=true
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}}
|
||||
<DataDisplayWrapper
|
||||
container_style=schema.get().container_style
|
||||
header_style=schema.get().header_style
|
||||
header_text=schema.get().header
|
||||
>
|
||||
<DataDisplayView display_data=display_value schema=schema />
|
||||
</DataDisplayWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
/// # Display Wrapper Component
|
||||
/// This component is designed to wrap information for the [`ValueDisplay`] component. It allows
|
||||
/// for simple uniform styling of the header and capsule, along with the ability for users to
|
||||
/// collapse the information.
|
||||
#[component]
|
||||
fn DisplayWrapper(
|
||||
#[prop(into, optional)] header: Signal<String>,
|
||||
#[prop(into, optional)] nested: Signal<bool>,
|
||||
fn DataDisplayWrapper(
|
||||
#[prop(into)] container_style: SchemaContainerStyle,
|
||||
#[prop(into)] header_style: SchemaHeaderStyle,
|
||||
#[prop(into)] header_text: Signal<String>,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
// This component takes a wrapper style and displays the "chrome" of the interface. It has two
|
||||
// options: Text Headers or Capsules. This will then render a DataDisplayHeader, which handles
|
||||
// the styling for the text inside the header specifically.
|
||||
|
||||
// The capsule style is a colored header and a rounded container.
|
||||
// The text header style is an invisible container.
|
||||
|
||||
let expanded = RwSignal::new(true);
|
||||
|
||||
view! {
|
||||
<div class="m-3 rounded-lg shadow-lg flex flex-col">
|
||||
<div
|
||||
class="p-2 px-3 font-bold rounded-t-lg cursor-pointer"
|
||||
class=(["bg-gray-300"], move || !nested.get())
|
||||
class=(["text-white", "bg-red-900"], move || nested.get())
|
||||
class=(["rounded-lg"], move || !expanded.get())
|
||||
on:click=move |_| expanded.update(|b| *b = !*b)
|
||||
>
|
||||
{header}
|
||||
</div>
|
||||
<div
|
||||
class="grid transition-all ease-in-out"
|
||||
style:grid-template-rows=move || if expanded.get() { "1fr" } else { "0fr" }
|
||||
>
|
||||
<div class="min-h-0 overflow-hidden">
|
||||
<div class="p-2">{children()}</div>
|
||||
match container_style {
|
||||
SchemaContainerStyle::Header => {
|
||||
// Creates a simple container that only has a header and a series of children.
|
||||
// The spacing between the header and children is controlled by the header style.
|
||||
// This type of container is not collapsable at the moment.
|
||||
view! {
|
||||
<div class="flex flex-col w-full text-left">
|
||||
<div class="self-start w-full">
|
||||
<DataDisplayHeader header_text=header_text header_style=header_style />
|
||||
<div class="p-2 flex flex-col gap-1">{children()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
/// # Primitive Value Display Component
|
||||
/// This component allows for the display of a `ValueType::String` or `ValueType::Number`.
|
||||
#[component]
|
||||
fn PrimitiveDisplay(
|
||||
#[prop(into)] header: Signal<String>,
|
||||
#[prop(into)] value: Signal<ValueType>,
|
||||
#[prop(into, optional)] nested: Signal<bool>,
|
||||
) -> impl IntoView {
|
||||
let display_string = Memo::new(move |_| {
|
||||
let value = value.get();
|
||||
match value {
|
||||
ValueType::String(_) | ValueType::Number(_) => value.to_string(),
|
||||
_ => "Value is not a string.".to_string(),
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
SchemaContainerStyle::Capsule => {
|
||||
// Creates a capsule container - it is collapsable, with rounded corners and a colored
|
||||
// header.
|
||||
view! {
|
||||
<div class="self-stretch rounded-lg shadow-lg flex flex-col w-full text-center">
|
||||
<div
|
||||
class="p-2 px-3 rounded-t-lg cursor-pointer bg-red-800 text-white"
|
||||
class=(["rounded-lg"], move || !expanded.get())
|
||||
on:click=move |_| expanded.update(|b| *b = !*b)
|
||||
>
|
||||
<DataDisplayHeader header_text=header_text header_style=header_style />
|
||||
</div>
|
||||
<div
|
||||
class="grid transition-all ease-in-out"
|
||||
style:grid-template-rows=move || if expanded.get() { "1fr" } else { "0fr" }
|
||||
>
|
||||
<div class="min-h-0 overflow-hidden">
|
||||
<div class="p-2 flex flex-col gap-1 flex-1">{children()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<DisplayWrapper header=header nested=nested>
|
||||
<div>{display_string}</div>
|
||||
</DisplayWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
/// # List Value Display Component
|
||||
/// This component allows for the display of a `ValueType::List`. It will show an error if passed
|
||||
/// anything other than a list. It requires an item template that tells the list how to display
|
||||
/// each value inside of it, most specifically the type of data inside of it.
|
||||
#[component]
|
||||
fn ListDisplay(
|
||||
#[prop(into)] item_template: Signal<Box<SchemaNode>>,
|
||||
#[prop(into)] value: Signal<ValueType>,
|
||||
#[prop(into)] header: Signal<String>,
|
||||
#[prop(into, optional)] nested: Signal<bool>,
|
||||
fn DataDisplayHeader(
|
||||
#[prop(into)] header_style: SchemaHeaderStyle,
|
||||
#[prop(into)] header_text: Signal<String>,
|
||||
) -> impl IntoView {
|
||||
// Unwrap the value and check if it's the correct type. To be displayed with this component, it
|
||||
// MUST be a ValueType::List.
|
||||
let display_list = Memo::new(move |_| {
|
||||
let ValueType::List(Some(list)) = value.get() else {
|
||||
return Vec::new();
|
||||
};
|
||||
// This component will take a header style and displays the corresponding text. It has a few
|
||||
// options, all of which are defined in the HeaderStyle enum.
|
||||
|
||||
list.into_iter()
|
||||
.enumerate()
|
||||
.collect::<Vec<(usize, ValueType)>>()
|
||||
});
|
||||
|
||||
let is_valid = Memo::new(move |_| matches!(value.get(), ValueType::List(_)));
|
||||
|
||||
let item_header = Memo::new(move |_| item_template.get().header);
|
||||
|
||||
view! {
|
||||
<DisplayWrapper header=header nested=nested>
|
||||
<For
|
||||
each=move || display_list.get()
|
||||
key=|(idx, _)| idx.clone()
|
||||
children=move |(idx, entry)| {
|
||||
match item_template.get().data_type {
|
||||
SchemaType::String | SchemaType::Number => {
|
||||
view! { <PrimitiveDisplay value=entry header=item_header /> }.into_any()
|
||||
}
|
||||
SchemaType::List => {
|
||||
view! {
|
||||
<div>"Nested lists are not currently supported for display."</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
SchemaType::Map => {
|
||||
let map_header = format!("{} {}", item_template.get().header, idx + 1);
|
||||
view! {
|
||||
<MapDisplay
|
||||
value=entry
|
||||
header=map_header
|
||||
schema=move || item_template.get().children
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
</DisplayWrapper>
|
||||
match header_style {
|
||||
SchemaHeaderStyle::MainHeader => {
|
||||
view! { <div class="text-3xl font-bold my-2">{header_text}</div> }.into_any()
|
||||
}
|
||||
SchemaHeaderStyle::SubsectionHeader => {
|
||||
view! { <div class="text-xl font-bold">{header_text}</div> }.into_any()
|
||||
}
|
||||
SchemaHeaderStyle::Bold => view! { <div class="font-bold">{header_text}</div> }.into_any(),
|
||||
SchemaHeaderStyle::None => view! {}.into_any(),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Map Data Display Component
|
||||
/// This component allows for the display of a `ValueType::Map`. The nested information must be defined
|
||||
/// by a [`SchemaNode`], which tells the display what keys in the value map to search for the
|
||||
/// display information.
|
||||
#[component]
|
||||
fn MapDisplay(
|
||||
#[prop(into)] value: Signal<ValueType>,
|
||||
#[prop(into)] header: Signal<String>,
|
||||
#[prop(into)] schema: Signal<IndexMap<String, SchemaNode>>,
|
||||
#[prop(into, optional)] nested: Signal<bool>,
|
||||
fn DataDisplayView(
|
||||
#[prop(into)] display_data: Signal<Option<ValueType>>,
|
||||
#[prop(into)] schema: Signal<SchemaNode>,
|
||||
) -> impl IntoView {
|
||||
// Unwrap the value and check if it is the correct type. It must be a ValueType::Map.
|
||||
let display_map = Memo::new(move |_| {
|
||||
let ValueType::Map(Some(map)) = value.get() else {
|
||||
return HashMap::new();
|
||||
};
|
||||
// This is the part of the data display that does most of the work. It will take the values
|
||||
// found in the DataDisplay component and perform the logic that actually displays the
|
||||
// information. This component is only displayed if there is Some(value) in the DataDisplay.
|
||||
|
||||
map
|
||||
});
|
||||
// In the case that this is displaying a Map or List, it will recursively call the DataDisplay
|
||||
// component.
|
||||
|
||||
Effect::new(move || {
|
||||
debug_log!("Current map values: {:?}", display_map.get());
|
||||
});
|
||||
// If this is displaying a PrimitiveList, it will flatten and join all the values into a single
|
||||
// string, then display that string.
|
||||
|
||||
let is_valid = Memo::new(move |_| matches!(value.get(), ValueType::Map(_)));
|
||||
let data_type = schema.get().data_type;
|
||||
|
||||
view! {
|
||||
<DisplayWrapper header=header nested=nested>
|
||||
<For
|
||||
each=move || schema.get()
|
||||
key=|(k, _)| k.clone()
|
||||
children=move |(member, child_schema)| {
|
||||
let display_value = Memo::new(move |_| {
|
||||
display_map.get().get(&member).map(|v| v.clone()).unwrap_or_default()
|
||||
});
|
||||
match child_schema.data_type {
|
||||
SchemaType::String | SchemaType::Number => {
|
||||
view! {
|
||||
<PrimitiveDisplay value=display_value header=child_schema.header />
|
||||
}
|
||||
.into_any()
|
||||
// Matches the specific data type. Displays errors if the given display data does not match.
|
||||
match data_type {
|
||||
SchemaType::String => {
|
||||
let value = Signal::derive(move || {
|
||||
display_data
|
||||
.get()
|
||||
.map(|v| v.as_string().ok().flatten())
|
||||
.flatten()
|
||||
.unwrap_or("N/A".to_string())
|
||||
});
|
||||
view! { <div>{value}</div> }.into_any()
|
||||
}
|
||||
SchemaType::Number => {
|
||||
let value = Signal::derive(move || {
|
||||
display_data
|
||||
.get()
|
||||
.map(|v| v.as_number().ok().flatten())
|
||||
.flatten()
|
||||
.unwrap_or("N/A".to_string())
|
||||
});
|
||||
view! { <div>{value}</div> }.into_any()
|
||||
}
|
||||
SchemaType::PrimitiveList => {
|
||||
let value = Signal::derive(move || {
|
||||
display_data
|
||||
.get()
|
||||
.map(|v| v.as_list().ok().flatten())
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|v| v.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
});
|
||||
view! { <div>{value}</div> }.into_any()
|
||||
}
|
||||
SchemaType::MapList => {
|
||||
let list = Signal::derive(move || {
|
||||
display_data
|
||||
.get()
|
||||
.map(|v| v.as_list().ok().flatten())
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter_map(|v| v.as_map().ok().flatten().clone())
|
||||
.collect::<Vec<HashMap<String, ValueType>>>()
|
||||
});
|
||||
let primary_key = StoredValue::new(schema.get().primary_key.unwrap_or_default());
|
||||
view! {
|
||||
<Show
|
||||
when=move || !list.get().is_empty()
|
||||
fallback=|| view! { <div class="w-full text-center">"List is empty."</div> }
|
||||
>
|
||||
<For
|
||||
each=move || list.get()
|
||||
key=move |map| {
|
||||
map.get(&primary_key.get_value())
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
SchemaType::List => {
|
||||
let template = child_schema
|
||||
.item_template
|
||||
.unwrap_or(Box::from(SchemaNode::new(SchemaType::List)));
|
||||
|
||||
children=move |map| {
|
||||
view! {
|
||||
<ListDisplay
|
||||
value=display_value
|
||||
item_template=template
|
||||
header=child_schema.header
|
||||
<DataDisplay
|
||||
schema=*schema.get().item_template.unwrap_or_default()
|
||||
data_map=map
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
SchemaType::Map => {
|
||||
/>
|
||||
</Show>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
SchemaType::Map => {
|
||||
let map = Signal::derive(move || {
|
||||
display_data
|
||||
.get()
|
||||
.map(|v| v.as_map().ok().flatten())
|
||||
.flatten()
|
||||
});
|
||||
view! {
|
||||
<Show
|
||||
when=move || map.get().is_some()
|
||||
fallback=|| view! { <div class="text-red-500">"Map was not found."</div> }
|
||||
>
|
||||
<For
|
||||
each=move || schema.get().children
|
||||
key=|(key, _)| key.clone()
|
||||
children=move |(key, schema)| {
|
||||
view! {
|
||||
<MapDisplay
|
||||
value=display_value
|
||||
schema=child_schema.children
|
||||
header=child_schema.header
|
||||
<DataDisplay
|
||||
schema=schema
|
||||
data_member=key
|
||||
data_map=Signal::derive(move || map.get().unwrap_or_default())
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
</DisplayWrapper>
|
||||
/>
|
||||
</Show>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user