use std::collections::BTreeMap;
use dioxus::prelude::*;
use dioxus_free_icons::{Icon, icons::ld_icons::*};
use crate::parser::{ApiOperation, ApiTag, OpenApiSpec, SchemaDefinition};
use super::schema_viewer::SchemaViewer;
use super::tag_group::{TagGroup, UngroupedEndpoints};
#[derive(Props, Clone, PartialEq)]
pub struct OpenApiViewerProps {
pub spec: OpenApiSpec,
#[props(default)]
pub tags: Option<Vec<String>>,
#[props(default = true)]
pub show_schemas: bool,
}
#[component]
pub fn OpenApiViewer(props: OpenApiViewerProps) -> Element {
let spec = &props.spec;
let (grouped_ops, ungrouped_ops) = group_operations_by_tag(&spec.operations, &spec.tags);
let filtered_groups: Vec<_> = if let Some(filter_tags) = &props.tags {
grouped_ops
.into_iter()
.filter(|(tag, _)| {
filter_tags
.iter()
.any(|t| t.eq_ignore_ascii_case(&tag.name))
})
.collect()
} else {
grouped_ops
};
rsx! {
div { class: "openapi-viewer",
ApiInfoHeader { info: spec.info.clone(), servers: spec.servers.clone() }
div { class: "mt-6",
for (tag, ops) in filtered_groups {
TagGroup {
key: "{tag.name}",
tag: tag.clone(),
operations: ops,
}
}
if props.tags.is_none() {
UngroupedEndpoints { operations: ungrouped_ops }
}
}
if props.show_schemas && !spec.schemas.is_empty() {
SchemaDefinitions { schemas: spec.schemas.clone() }
}
}
}
}
fn group_operations_by_tag(
operations: &[ApiOperation],
tags: &[ApiTag],
) -> (Vec<(ApiTag, Vec<ApiOperation>)>, Vec<ApiOperation>) {
let mut grouped: BTreeMap<String, Vec<ApiOperation>> = BTreeMap::new();
let mut ungrouped = Vec::new();
for op in operations {
if op.tags.is_empty() {
ungrouped.push(op.clone());
} else {
for tag_name in &op.tags {
grouped
.entry(tag_name.clone())
.or_default()
.push(op.clone());
}
}
}
let mut result = Vec::new();
for tag in tags {
if let Some(ops) = grouped.remove(&tag.name) {
result.push((tag.clone(), ops));
}
}
for (tag_name, ops) in grouped {
result.push((
ApiTag {
name: tag_name,
description: None,
},
ops,
));
}
(result, ungrouped)
}
#[derive(Props, Clone, PartialEq)]
pub struct ApiInfoHeaderProps {
pub info: crate::parser::ApiInfo,
pub servers: Vec<crate::parser::ApiServer>,
}
#[component]
pub fn ApiInfoHeader(props: ApiInfoHeaderProps) -> Element {
let info = &props.info;
rsx! {
div { class: "border-b border-base-300 pb-4 mb-4",
div { class: "flex items-center gap-3 flex-wrap",
h2 { class: "text-2xl font-bold text-base-content",
"{info.title}"
}
span { class: "badge badge-primary badge-outline",
"v{info.version}"
}
}
if let Some(desc) = &info.description {
p { class: "mt-2 text-base-content/70",
"{desc}"
}
}
if !props.servers.is_empty() {
div { class: "mt-4",
span { class: "text-sm font-semibold text-base-content/60 flex items-center gap-2",
Icon { class: "size-4", icon: LdServer }
"Servers"
}
div { class: "mt-2 space-y-1",
for server in &props.servers {
div { class: "flex items-center gap-2",
code { class: "text-sm font-mono text-primary bg-base-200 px-2 py-1 rounded",
"{server.url}"
}
if let Some(desc) = &server.description {
span { class: "text-sm text-base-content/50",
"- {desc}"
}
}
}
}
}
}
}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct SchemaDefinitionsProps {
pub schemas: BTreeMap<String, SchemaDefinition>,
}
#[component]
pub fn SchemaDefinitions(props: SchemaDefinitionsProps) -> Element {
let mut is_expanded = use_signal(|| false);
rsx! {
div { class: "mt-8 border-t border-base-300 pt-4",
button {
class: "w-full flex items-center gap-2 py-2 text-left",
onclick: move |_| is_expanded.set(!is_expanded()),
Icon {
class: if is_expanded() { "size-5 text-base-content/50 transform rotate-90 transition-transform" } else { "size-5 text-base-content/50 transition-transform" },
icon: LdChevronRight
}
h3 { class: "text-lg font-semibold text-base-content flex items-center gap-2",
Icon { class: "size-5", icon: LdBraces }
"Schema Definitions"
}
span { class: "badge badge-ghost badge-sm",
"{props.schemas.len()}"
}
}
if is_expanded() {
div { class: "mt-4 space-y-4",
for (name, schema) in &props.schemas {
div { class: "border border-base-300 rounded-lg overflow-hidden",
div { class: "px-4 py-2 bg-base-200 border-b border-base-300",
code { class: "font-mono font-semibold text-primary",
"{name}"
}
}
div { class: "p-4",
SchemaViewer {
schema: schema.clone(),
expanded: true,
}
}
}
}
}
}
}
}
}