dioxus_mdx/components/openapi/
spec_viewer.rs1use std::collections::BTreeMap;
4
5use dioxus::prelude::*;
6use dioxus_free_icons::{Icon, icons::ld_icons::*};
7
8use crate::parser::{ApiOperation, ApiTag, OpenApiSpec, SchemaDefinition};
9
10use super::schema_viewer::SchemaViewer;
11use super::tag_group::{TagGroup, UngroupedEndpoints};
12
13#[derive(Props, Clone, PartialEq)]
15pub struct OpenApiViewerProps {
16 pub spec: OpenApiSpec,
18 #[props(default)]
20 pub tags: Option<Vec<String>>,
21 #[props(default = true)]
23 pub show_schemas: bool,
24}
25
26#[component]
28pub fn OpenApiViewer(props: OpenApiViewerProps) -> Element {
29 let spec = &props.spec;
30
31 let (grouped_ops, ungrouped_ops) = group_operations_by_tag(&spec.operations, &spec.tags);
33
34 let filtered_groups: Vec<_> = if let Some(filter_tags) = &props.tags {
36 grouped_ops
37 .into_iter()
38 .filter(|(tag, _)| {
39 filter_tags
40 .iter()
41 .any(|t| t.eq_ignore_ascii_case(&tag.name))
42 })
43 .collect()
44 } else {
45 grouped_ops
46 };
47
48 rsx! {
49 div { class: "openapi-viewer",
50 ApiInfoHeader { info: spec.info.clone(), servers: spec.servers.clone() }
52
53 div { class: "mt-6",
55 for (tag, ops) in filtered_groups {
56 TagGroup {
57 key: "{tag.name}",
58 tag: tag.clone(),
59 operations: ops,
60 }
61 }
62
63 if props.tags.is_none() {
65 UngroupedEndpoints { operations: ungrouped_ops }
66 }
67 }
68
69 if props.show_schemas && !spec.schemas.is_empty() {
71 SchemaDefinitions { schemas: spec.schemas.clone() }
72 }
73 }
74 }
75}
76
77fn group_operations_by_tag(
79 operations: &[ApiOperation],
80 tags: &[ApiTag],
81) -> (Vec<(ApiTag, Vec<ApiOperation>)>, Vec<ApiOperation>) {
82 let mut grouped: BTreeMap<String, Vec<ApiOperation>> = BTreeMap::new();
83 let mut ungrouped = Vec::new();
84
85 for op in operations {
86 if op.tags.is_empty() {
87 ungrouped.push(op.clone());
88 } else {
89 for tag_name in &op.tags {
90 grouped
91 .entry(tag_name.clone())
92 .or_default()
93 .push(op.clone());
94 }
95 }
96 }
97
98 let mut result = Vec::new();
100 for tag in tags {
101 if let Some(ops) = grouped.remove(&tag.name) {
102 result.push((tag.clone(), ops));
103 }
104 }
105
106 for (tag_name, ops) in grouped {
108 result.push((
109 ApiTag {
110 name: tag_name,
111 description: None,
112 },
113 ops,
114 ));
115 }
116
117 (result, ungrouped)
118}
119
120#[derive(Props, Clone, PartialEq)]
122pub struct ApiInfoHeaderProps {
123 pub info: crate::parser::ApiInfo,
125 pub servers: Vec<crate::parser::ApiServer>,
127}
128
129#[component]
131pub fn ApiInfoHeader(props: ApiInfoHeaderProps) -> Element {
132 let info = &props.info;
133
134 rsx! {
135 div { class: "border-b border-base-300 pb-4 mb-4",
136 div { class: "flex items-center gap-3 flex-wrap",
138 h2 { class: "text-2xl font-bold text-base-content",
139 "{info.title}"
140 }
141 span { class: "badge badge-primary badge-outline",
142 "v{info.version}"
143 }
144 }
145
146 if let Some(desc) = &info.description {
148 p { class: "mt-2 text-base-content/70",
149 "{desc}"
150 }
151 }
152
153 if !props.servers.is_empty() {
155 div { class: "mt-4",
156 span { class: "text-sm font-semibold text-base-content/60 flex items-center gap-2",
157 Icon { class: "size-4", icon: LdServer }
158 "Servers"
159 }
160 div { class: "mt-2 space-y-1",
161 for server in &props.servers {
162 div { class: "flex items-center gap-2",
163 code { class: "text-sm font-mono text-primary bg-base-200 px-2 py-1 rounded",
164 "{server.url}"
165 }
166 if let Some(desc) = &server.description {
167 span { class: "text-sm text-base-content/50",
168 "- {desc}"
169 }
170 }
171 }
172 }
173 }
174 }
175 }
176 }
177 }
178}
179
180#[derive(Props, Clone, PartialEq)]
182pub struct SchemaDefinitionsProps {
183 pub schemas: BTreeMap<String, SchemaDefinition>,
185}
186
187#[component]
189pub fn SchemaDefinitions(props: SchemaDefinitionsProps) -> Element {
190 let mut is_expanded = use_signal(|| false);
191
192 rsx! {
193 div { class: "mt-8 border-t border-base-300 pt-4",
194 button {
196 class: "w-full flex items-center gap-2 py-2 text-left",
197 onclick: move |_| is_expanded.set(!is_expanded()),
198
199 Icon {
200 class: if is_expanded() { "size-5 text-base-content/50 transform rotate-90 transition-transform" } else { "size-5 text-base-content/50 transition-transform" },
201 icon: LdChevronRight
202 }
203
204 h3 { class: "text-lg font-semibold text-base-content flex items-center gap-2",
205 Icon { class: "size-5", icon: LdBraces }
206 "Schema Definitions"
207 }
208
209 span { class: "badge badge-ghost badge-sm",
210 "{props.schemas.len()}"
211 }
212 }
213
214 if is_expanded() {
216 div { class: "mt-4 space-y-4",
217 for (name, schema) in &props.schemas {
218 div { class: "border border-base-300 rounded-lg overflow-hidden",
219 div { class: "px-4 py-2 bg-base-200 border-b border-base-300",
221 code { class: "font-mono font-semibold text-primary",
222 "{name}"
223 }
224 }
225 div { class: "p-4",
227 SchemaViewer {
228 schema: schema.clone(),
229 expanded: true,
230 }
231 }
232 }
233 }
234 }
235 }
236 }
237 }
238}