fastapi-voyager 0.9.4__tar.gz → 0.10.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/PKG-INFO +19 -18
  2. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/README.md +18 -17
  3. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/render.py +5 -5
  4. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/server.py +2 -2
  5. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/version.py +1 -1
  6. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/voyager.py +83 -58
  7. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/index.html +67 -37
  8. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/vue-main.js +57 -3
  9. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/tests/demo.py +5 -1
  10. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/tests/programatic.py +1 -0
  11. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/.gitignore +0 -0
  12. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/.python-version +0 -0
  13. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/LICENSE +0 -0
  14. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/pyproject.toml +0 -0
  15. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/__init__.py +0 -0
  16. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/cli.py +0 -0
  17. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/filter.py +0 -0
  18. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/module.py +0 -0
  19. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/type.py +0 -0
  20. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/type_helper.py +0 -0
  21. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/component/render-graph.js +0 -0
  22. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
  23. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
  24. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/component/schema-field-filter.js +0 -0
  25. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/graph-ui.js +0 -0
  26. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
  27. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
  28. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
  29. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
  30. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
  31. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
  32. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
  33. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
  34. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
  35. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/quasar.min.css +0 -0
  36. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/src/fastapi_voyager/web/quasar.min.js +0 -0
  37. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/tests/__init__.py +0 -0
  38. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/tests/demo_anno.py +0 -0
  39. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/tests/service/__init__.py +0 -0
  40. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/tests/service/schema.py +0 -0
  41. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/tests/test_analysis.py +0 -0
  42. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/tests/test_filter.py +0 -0
  43. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/tests/test_generic.py +0 -0
  44. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/tests/test_import.py +0 -0
  45. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/tests/test_module.py +0 -0
  46. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/tests/test_type_helper.py +0 -0
  47. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/uv.lock +0 -0
  48. {fastapi_voyager-0.9.4 → fastapi_voyager-0.10.1}/voyager.jpg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.9.4
3
+ Version: 0.10.1
4
4
  Summary: Visualize FastAPI application's routing tree and dependencies
5
5
  Project-URL: Homepage, https://github.com/allmonday/fastapi-voyager
6
6
  Project-URL: Source, https://github.com/allmonday/fastapi-voyager
@@ -163,18 +163,19 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
163
163
 
164
164
  ### backlog
165
165
  - [ ] user can generate nodes/edges manually and connect to generated ones
166
- - [ ] add owner
166
+ - [ ] eg: add owner
167
167
  - [ ] add extra info for schema
168
168
  - [ ] display standard ER diagram `hard`
169
169
  - [ ] display potential invalid links
170
- - [ ] support dataclass (pending)
170
+ - [ ] optimize static resource (allow manually config url)
171
+ - [ ] improve search dialog
172
+ - [ ] add route/tag list
171
173
 
172
174
  ### in analysis
173
175
  - [ ] click field to highlight links
174
176
  - [ ] animation effect for edges
175
177
  - [ ] customrized right click panel
176
178
  - [ ] show own dependencies
177
- - [ ] clean up fe code
178
179
 
179
180
  ### plan:
180
181
  #### <0.9:
@@ -221,24 +222,24 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
221
222
  - [x] schema detail panel show fields by default
222
223
  - [x] adjust schema panel's height
223
224
  - [x] show from base information in subset case
225
+ - 0.9.5
226
+ - [x] route list should have a max height
224
227
 
225
228
  #### 0.10
229
+ - [x] refactor voyager.py tag -> route structure
230
+ - [x] fix missing route (tag has only one route which return primitive value)
231
+ - [x] make right panel resizable by dragging
232
+ - [x] allow closing tag expansion item
233
+ - [x] hide brief mode if not configured
234
+ - [x] add focus button to only show related nodes under current route/tag graph in dialog
235
+
236
+ #### 0.11
237
+ - [ ] enable/disable module cluster (to save space)
238
+ - [ ] fix layout issue when rendering huge graph
239
+ - [ ] logging information
226
240
  - [ ] support opening route in swagger
227
241
  - config docs path
228
- - [ ] add http method for route
229
- - [ ] enable/disable module cluster (may save space)
230
- - [ ] logging information
231
242
  - [ ] add tests
232
- - [ ] hide brief mode if not configured
233
- - [ ] optimize static resource
234
- - [ ] show route count in tag expansion item
235
- - [ ] route list show have a max height to trigger scrollable
236
- - [ ] fix layout issue when rendering huge graph
237
-
238
- #### 0.11
239
- - [ ] improve user experience
240
- - double click to show detail
241
- - improve search dialog
242
243
 
243
244
  #### 0.12
244
245
  - [ ] integration with pydantic-resolve
@@ -247,7 +248,7 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
247
248
 
248
249
  #### 0.13
249
250
  - [ ] config release pipeline
250
- - [ ]
251
+
251
252
 
252
253
  ## Using with pydantic-resolve
253
254
 
@@ -136,18 +136,19 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
136
136
 
137
137
  ### backlog
138
138
  - [ ] user can generate nodes/edges manually and connect to generated ones
139
- - [ ] add owner
139
+ - [ ] eg: add owner
140
140
  - [ ] add extra info for schema
141
141
  - [ ] display standard ER diagram `hard`
142
142
  - [ ] display potential invalid links
143
- - [ ] support dataclass (pending)
143
+ - [ ] optimize static resource (allow manually config url)
144
+ - [ ] improve search dialog
145
+ - [ ] add route/tag list
144
146
 
145
147
  ### in analysis
146
148
  - [ ] click field to highlight links
147
149
  - [ ] animation effect for edges
148
150
  - [ ] customrized right click panel
149
151
  - [ ] show own dependencies
150
- - [ ] clean up fe code
151
152
 
152
153
  ### plan:
153
154
  #### <0.9:
@@ -194,24 +195,24 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
194
195
  - [x] schema detail panel show fields by default
195
196
  - [x] adjust schema panel's height
196
197
  - [x] show from base information in subset case
198
+ - 0.9.5
199
+ - [x] route list should have a max height
197
200
 
198
201
  #### 0.10
202
+ - [x] refactor voyager.py tag -> route structure
203
+ - [x] fix missing route (tag has only one route which return primitive value)
204
+ - [x] make right panel resizable by dragging
205
+ - [x] allow closing tag expansion item
206
+ - [x] hide brief mode if not configured
207
+ - [x] add focus button to only show related nodes under current route/tag graph in dialog
208
+
209
+ #### 0.11
210
+ - [ ] enable/disable module cluster (to save space)
211
+ - [ ] fix layout issue when rendering huge graph
212
+ - [ ] logging information
199
213
  - [ ] support opening route in swagger
200
214
  - config docs path
201
- - [ ] add http method for route
202
- - [ ] enable/disable module cluster (may save space)
203
- - [ ] logging information
204
215
  - [ ] add tests
205
- - [ ] hide brief mode if not configured
206
- - [ ] optimize static resource
207
- - [ ] show route count in tag expansion item
208
- - [ ] route list show have a max height to trigger scrollable
209
- - [ ] fix layout issue when rendering huge graph
210
-
211
- #### 0.11
212
- - [ ] improve user experience
213
- - double click to show detail
214
- - improve search dialog
215
216
 
216
217
  #### 0.12
217
218
  - [ ] integration with pydantic-resolve
@@ -220,7 +221,7 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
220
221
 
221
222
  #### 0.13
222
223
  - [ ] config release pipeline
223
- - [ ]
224
+
224
225
 
225
226
  ## Using with pydantic-resolve
226
227
 
@@ -62,9 +62,8 @@ class Renderer:
62
62
  else:
63
63
  raise ValueError(f'Unknown link type: {link.type}')
64
64
 
65
- def render_module_schema_wrapper(self, mods: list[ModuleNode]) -> str:
65
+ def render_module_schema_content(self, mods: list[ModuleNode]) -> str:
66
66
  module_color_flag = set(self.module_color.keys())
67
- print(module_color_flag)
68
67
 
69
68
  def render_module_schema(mod: ModuleNode) -> str:
70
69
  color: Optional[str] = None
@@ -104,7 +103,7 @@ class Renderer:
104
103
  inner_nodes = [
105
104
  f'''
106
105
  "{r.id}" [
107
- label = " {r.name}: {r.response_schema} "
106
+ label = " {r.name} | {r.response_schema} "
108
107
  margin="0.5,0.1"
109
108
  shape = "record"
110
109
  ];''' for r in mod.routes
@@ -122,7 +121,7 @@ class Renderer:
122
121
  {child_str}
123
122
  }}'''
124
123
 
125
- def render_dot(self, tags: list[Tag], routes: list[Route], nodes: list[SchemaNode], links: list[Link]) -> str:
124
+ def render_dot(self, tags: list[Tag], routes: list[Route], nodes: list[SchemaNode], links: list[Link], spline_line=False) -> str:
126
125
  module_schemas = build_module_schema_tree(nodes)
127
126
  module_routes = build_module_route_tree(routes)
128
127
 
@@ -136,7 +135,7 @@ class Renderer:
136
135
  ])
137
136
 
138
137
 
139
- module_schemas_str = self.render_module_schema_wrapper(module_schemas)
138
+ module_schemas_str = self.render_module_schema_content(module_schemas)
140
139
  module_routes_str = '\n'.join(self.render_module_route(m) for m in module_routes)
141
140
  link_str = '\n'.join(self.render_link(link) for link in links)
142
141
 
@@ -144,6 +143,7 @@ class Renderer:
144
143
  digraph world {{
145
144
  pad="0.5"
146
145
  nodesep=0.8
146
+ {'splines=line' if spline_line else ''}
147
147
  fontname="Helvetica,Arial,sans-serif"
148
148
  node [fontname="Helvetica,Arial,sans-serif"]
149
149
  edge [
@@ -19,6 +19,7 @@ class OptionParam(BaseModel):
19
19
  tags: list[Tag]
20
20
  schemas: list[SchemaNode]
21
21
  dot: str
22
+ enable_brief_mode: bool
22
23
 
23
24
  class Payload(BaseModel):
24
25
  tags: Optional[list[str]] = None
@@ -53,11 +54,10 @@ def create_route(
53
54
  schemas = voyager.nodes[:]
54
55
  schemas.sort(key=lambda s: s.name)
55
56
 
56
- return OptionParam(tags=tags, schemas=schemas, dot=dot)
57
+ return OptionParam(tags=tags, schemas=schemas, dot=dot, enable_brief_mode=bool(module_prefix))
57
58
 
58
59
  @router.post("/dot", response_class=PlainTextResponse)
59
60
  def get_filtered_dot(payload: Payload) -> str:
60
- print(payload)
61
61
  voyager = Voyager(
62
62
  include_tags=payload.tags,
63
63
  schema=payload.schema_name,
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.9.4"
2
+ __version__ = "0.10.1"
@@ -1,4 +1,4 @@
1
- import inspect
1
+ from pydantic import BaseModel
2
2
  from fastapi import FastAPI, routing
3
3
  from fastapi_voyager.type_helper import (
4
4
  get_core_types,
@@ -54,9 +54,14 @@ class Voyager:
54
54
 
55
55
  def _get_available_route(self, app: FastAPI):
56
56
  for route in app.routes:
57
- if isinstance(route, routing.APIRoute) and route.response_model:
57
+ if isinstance(route, routing.APIRoute):
58
58
  yield route
59
59
 
60
+ def analysis_route(self, route: routing.APIRoute):
61
+ ...
62
+
63
+ def analysis_tags(self, tag: str):
64
+ ...
60
65
 
61
66
  def analysis(self, app: FastAPI):
62
67
  """
@@ -68,67 +73,76 @@ class Voyager:
68
73
  """
69
74
  schemas: list[type[BaseModel]] = []
70
75
 
76
+ # First, group all routes by tag
77
+ routes_by_tag: dict[str, list] = {}
71
78
  for route in self._get_available_route(app):
72
- # check tags
73
79
  tags = getattr(route, 'tags', None)
74
- route_tag = tags[0] if tags else '__default__'
75
- if self.include_tags and route_tag not in self.include_tags:
76
- continue
77
80
 
78
- # add tag if not exists
79
- tag_id = f'tag__{route_tag}'
80
- if tag_id not in self.tag_set:
81
- tag_obj = Tag(id=tag_id, name=route_tag, routes=[])
82
- self.tag_set[tag_id] = tag_obj
83
- self.tags.append(tag_obj)
84
-
85
- # add route and create links
86
- route_id = full_class_name(route.endpoint)
87
- route_name = route.endpoint.__name__
88
- route_module = route.endpoint.__module__
89
-
90
- # filter by route_name (route.id) if provided
91
- if self.route_name is not None and route_id != self.route_name:
92
- continue
81
+ # using multiple tags is harmful, it's not recommended and will not be supported
82
+ route_tag = tags[0] if tags else '__default__'
83
+ routes_by_tag.setdefault(route_tag, []).append(route)
93
84
 
94
- is_primitive_response = is_non_pydantic_type(route.response_model)
95
- # filter primitive route if needed
96
- if self.hide_primitive_route and is_primitive_response:
97
- continue
85
+ # Then filter by include_tags if provided
86
+ if self.include_tags:
87
+ filtered_routes_by_tag = {tag: routes for tag, routes in routes_by_tag.items()
88
+ if tag in self.include_tags}
89
+ else:
90
+ filtered_routes_by_tag = routes_by_tag
98
91
 
99
- self.links.append(Link(
100
- source=tag_id,
101
- source_origin=tag_id,
102
- target=route_id,
103
- target_origin=route_id,
104
- type='tag_route'
105
- ))
92
+ # Process filtered routes
93
+ for route_tag, routes in filtered_routes_by_tag.items():
106
94
 
107
- route_obj = Route(
108
- id=route_id,
109
- name=route_name,
110
- module=route_module,
111
- response_schema=get_type_name(route.response_model),
112
- is_primitive=is_primitive_response
113
- )
114
- self.routes.append(route_obj)
115
- # add route into current tag
116
- self.tag_set[tag_id].routes.append(route_obj)
117
-
118
- # add response_models and create links from route -> response_model
119
- for schema in get_core_types(route.response_model):
120
- if schema and issubclass(schema, BaseModel):
121
- is_primitive_response = False
122
- target_name = full_class_name(schema)
123
- self.links.append(Link(
124
- source=route_id,
125
- source_origin=route_id,
126
- target=self.generate_node_head(target_name),
127
- target_origin=target_name,
128
- type='route_to_schema'
129
- ))
130
-
131
- schemas.append(schema)
95
+ tag_id = f'tag__{route_tag}'
96
+ tag_obj = Tag(id=tag_id, name=route_tag, routes=[])
97
+ self.tags.append(tag_obj)
98
+
99
+ for route in routes:
100
+ # add route and create links
101
+ route_id = full_class_name(route.endpoint)
102
+ route_name = route.endpoint.__name__
103
+ route_module = route.endpoint.__module__
104
+
105
+ # filter by route_name (route.id) if provided
106
+ if self.route_name is not None and route_id != self.route_name:
107
+ continue
108
+
109
+ is_primitive_response = is_non_pydantic_type(route.response_model)
110
+ # filter primitive route if needed
111
+ if self.hide_primitive_route and is_primitive_response:
112
+ continue
113
+
114
+ self.links.append(Link(
115
+ source=tag_id,
116
+ source_origin=tag_id,
117
+ target=route_id,
118
+ target_origin=route_id,
119
+ type='tag_route'
120
+ ))
121
+
122
+ route_obj = Route(
123
+ id=route_id,
124
+ name=route_name,
125
+ module=route_module,
126
+ response_schema=get_type_name(route.response_model),
127
+ is_primitive=is_primitive_response
128
+ )
129
+ self.routes.append(route_obj)
130
+ tag_obj.routes.append(route_obj)
131
+
132
+ # add response_models and create links from route -> response_model
133
+ for schema in get_core_types(route.response_model):
134
+ if schema and issubclass(schema, BaseModel):
135
+ is_primitive_response = False
136
+ target_name = full_class_name(schema)
137
+ self.links.append(Link(
138
+ source=route_id,
139
+ source_origin=route_id,
140
+ target=self.generate_node_head(target_name),
141
+ target_origin=target_name,
142
+ type='route_to_schema'
143
+ ))
144
+
145
+ schemas.append(schema)
132
146
 
133
147
  for s in schemas:
134
148
  self.analysis_schemas(s)
@@ -271,6 +285,12 @@ class Voyager:
271
285
  schema=self.schema
272
286
  )
273
287
 
288
+ def handle_hide(self, tags, routes, links):
289
+ if self.include_tags:
290
+ return [], routes, [lk for lk in links if lk.type != 'tag_route']
291
+ else:
292
+ return tags, routes, links
293
+
274
294
  def render_dot(self):
275
295
  _tags, _routes, _nodes, _links = filter_graph(
276
296
  schema=self.schema,
@@ -281,7 +301,10 @@ class Voyager:
281
301
  links=self.links,
282
302
  node_set=self.node_set,
283
303
  )
304
+
284
305
  renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema)
306
+
307
+ _tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
285
308
  return renderer.render_dot(_tags, _routes, _nodes, _links)
286
309
 
287
310
  def render_brief_dot(self, module_prefix: str | None = None):
@@ -293,4 +316,6 @@ class Voyager:
293
316
  links=self.links,
294
317
  )
295
318
  renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=None)
296
- return renderer.render_dot(_tags, _routes, _nodes, _links)
319
+
320
+ _tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
321
+ return renderer.render_dot(_tags, _routes, _nodes, _links, True)
@@ -86,6 +86,7 @@
86
86
  </div>
87
87
  <div class="col-auto q-ml-auto">
88
88
  <q-toggle
89
+ v-if="state.enableBriefMode"
89
90
  class="q-mr-md"
90
91
  v-model="state.brief"
91
92
  label="Brief Mode"
@@ -163,12 +164,29 @@
163
164
 
164
165
  <q-drawer
165
166
  v-model="state.detailDrawer"
166
- width="500"
167
+ :width="state.drawerWidth"
167
168
  side="right"
169
+ style="border-left: 1px solid #888;"
168
170
  overlay
169
171
  bordered
170
172
  >
171
- <div style="z-index: 1; position: absolute; left: -17px; top: 9px">
173
+ <!-- 可拖拽的调整栏 -->
174
+ <div
175
+ @mousedown="startDragDrawer"
176
+ style="
177
+ position: absolute;
178
+ left: -3px;
179
+ top: 0;
180
+ width: 6px;
181
+ height: 100%;
182
+ cursor: col-resize;
183
+ background: transparent;
184
+ z-index: 10;
185
+ "
186
+ title="drag to resize"
187
+ ></div>
188
+
189
+ <div style="z-index: 11; position: absolute; left: -17px; top: 9px">
172
190
  <q-btn
173
191
  @click="state.detailDrawer = !state.detailDrawer"
174
192
  round
@@ -208,60 +226,69 @@
208
226
  v-for="tag in state.rawTags"
209
227
  :key="tag.name"
210
228
  expand-separator
211
- :model-value="state.tag === tag.name"
229
+ :model-value="state._tag === tag.name"
212
230
  @update:model-value="(val) => toggleTag(tag.name, val)"
213
231
  :header-class="state.tag === tag.name ? 'text-primary text-bold' : ''"
214
232
  content-class="q-pa-none"
215
233
  >
216
234
  <template #header>
217
235
  <div class="row items-center" style="width: 100%">
218
- <div class="row items-end">
236
+ <div class="row items-center">
219
237
  <q-icon
220
238
  class="q-mr-sm"
221
239
  :name="state.tag == tag.name ? 'folder' : 'folder_open'"
222
240
  ></q-icon>
223
- <span>{{ tag.name }}</span>
241
+ <span>{{ tag.name }} <q-chip class="q-ml-sm" dense>{{ tag.routes.length }}</q-chip></span>
224
242
  </div>
225
243
  </div>
226
244
  </template>
227
-
228
- <q-list separator>
229
- <q-item
230
- v-for="route in (state.hidePrimitiveRoute ? tag.routes.filter(r => !r.is_primitive) :tag.routes || [])"
231
- :key="route.id"
232
- clickable
233
- v-ripple
234
- :active="state.routeId === route.id"
235
- active-class=""
236
- @click="selectRoute(route.id)"
237
- >
238
- <q-item-section>
239
- <span class="q-ml-lg" style="white-space: nowrap">
240
- <q-icon
241
- class="q-mr-sm"
242
- name="data_object"
243
- ></q-icon>
244
- {{ route.name }}
245
- </span>
246
- </q-item-section>
247
- </q-item>
248
- <q-item
249
- v-if="!tag.routes || tag.routes.length === 0"
250
- dense
251
- >
252
- <q-item-section class="text-grey-6"
253
- >No routes</q-item-section
245
+ <q-list separator style="overflow: auto; max-height: 60vh;">
246
+ <q-item
247
+ v-for="route in (state.hidePrimitiveRoute ? tag.routes.filter(r => !r.is_primitive) :tag.routes || [])"
248
+ :key="route.id"
249
+ clickable
250
+ v-ripple
251
+ :active="state.routeId === route.id"
252
+ active-class=""
253
+ @click="selectRoute(route.id)"
254
254
  >
255
- </q-item>
256
- </q-list>
255
+ <q-item-section>
256
+ <span class="q-ml-lg" style="white-space: nowrap">
257
+ <q-icon
258
+ class="q-mr-sm"
259
+ name="data_object"
260
+ ></q-icon>
261
+ {{ route.name }}
262
+ </span>
263
+ </q-item-section>
264
+ </q-item>
265
+ <q-item
266
+ v-if="!tag.routes || tag.routes.length === 0"
267
+ dense
268
+ >
269
+ <q-item-section class="text-grey-6"
270
+ >No routes</q-item-section
271
+ >
272
+ </q-item>
273
+ </q-list>
274
+ </q-scroll-area>
257
275
  </q-expansion-item>
258
276
  </q-list>
259
- </q-scroll-area>
260
277
  </div>
261
278
  </template>
262
279
 
263
280
  <template #after>
264
- <div id="graph" class="fit"></div>
281
+ <div style="position: relative; width: 100%; height: 100%;">
282
+ <div id="graph" class="fit"></div>
283
+ <q-toggle
284
+ v-model="state.focus"
285
+ v-show="schemaCodeName"
286
+ @update:model-value="val => onFocusChange(val)"
287
+ label="Focus"
288
+ style="position: absolute; left: 8px; top: 8px; z-index: 10; background: rgba(255,255,255,0.85); border-radius: 4px; padding: 2px 8px;"
289
+ size="sm"
290
+ />
291
+ </div>
265
292
  </template>
266
293
  </q-splitter>
267
294
  </q-page-container>
@@ -291,7 +318,10 @@
291
318
 
292
319
  <q-dialog v-model="showRouteDetail" seamless position="bottom">
293
320
  <q-card style="width: 1100px; max-width: 1100px; max-height: 40vh">
294
- <route-code-display :route-id="routeCodeId" @close="showRouteDetail=false" />
321
+ <route-code-display
322
+ :route-id="routeCodeId"
323
+ @close="showRouteDetail=false"
324
+ />
295
325
  </q-card>
296
326
  </q-dialog>
297
327
 
@@ -10,6 +10,7 @@ const app = createApp({
10
10
  const state = reactive({
11
11
  // options and selections
12
12
  tag: null, // picked tag
13
+ _tag: null, // display tag
13
14
  routeId: null, // picked route
14
15
  schemaId: null, // picked schema
15
16
  showFields: "object",
@@ -18,7 +19,9 @@ const app = createApp({
18
19
  { label: "Object fields", value: "object" },
19
20
  { label: "All fields", value: "all" },
20
21
  ],
22
+ enableBriefMode: false,
21
23
  brief: false,
24
+ focus: false,
22
25
  hidePrimitiveRoute: false,
23
26
  generating: false,
24
27
  rawTags: [], // [{ name, routes: [{ id, name }] }]
@@ -28,6 +31,7 @@ const app = createApp({
28
31
  // Splitter size (left panel width in px)
29
32
  splitter: 300,
30
33
  detailDrawer: false,
34
+ drawerWidth: 500, // drawer 宽度
31
35
  });
32
36
 
33
37
  const showDetail = ref(false);
@@ -70,6 +74,7 @@ const app = createApp({
70
74
  acc[r.id] = r;
71
75
  return acc;
72
76
  }, {});
77
+ state.enableBriefMode = data.enable_brief_mode || false;
73
78
 
74
79
  // default route options placeholder
75
80
  } catch (e) {
@@ -79,18 +84,30 @@ const app = createApp({
79
84
  }
80
85
  }
81
86
 
82
- async function onGenerate(resetZoom = true) {
87
+ async function onFocusChange(val) {
88
+ if (val) {
89
+ await onGenerate(false, schemaCodeName.value)
90
+ } else {
91
+ await onGenerate(false, null)
92
+ setTimeout(() => {
93
+ const ele = $(`[data-name='${schemaCodeName.value}'] polygon`)
94
+ debugger
95
+ ele.click()
96
+ }, 1)
97
+ }
98
+ }
99
+
100
+ async function onGenerate(resetZoom = true, schema_name = null) {
83
101
  state.generating = true;
84
102
  try {
85
103
  const payload = {
86
104
  tags: state.tag ? [state.tag] : null,
87
- schema_name: state.schemaId || null,
105
+ schema_name: schema_name || null,
88
106
  route_name: state.routeId || null,
89
107
  show_fields: state.showFields,
90
108
  brief: state.brief,
91
109
  hide_primitive_route: state.hidePrimitiveRoute,
92
110
  };
93
-
94
111
  const res = await fetch("dot", {
95
112
  method: "POST",
96
113
  headers: { "Content-Type": "application/json" },
@@ -119,6 +136,7 @@ const app = createApp({
119
136
  resetCb: () => {
120
137
  state.detailDrawer = false;
121
138
  showRouteDetail.value = false;
139
+ schemaCodeName.value = ''
122
140
  }
123
141
  });
124
142
 
@@ -198,15 +216,23 @@ const app = createApp({
198
216
  state.schemaId = null;
199
217
  // state.showFields = "object";
200
218
  state.brief = false;
219
+ state.focus = false
220
+ schemaCodeName.value = ''
201
221
  onGenerate();
202
222
  }
203
223
 
204
224
  function toggleTag(tagName, expanded = null) {
205
225
  if (expanded === true) {
226
+ state._tag = tagName;
206
227
  state.tag = tagName;
207
228
  state.routeId = "";
229
+ state.focus = false
230
+ schemaCodeName.value = ''
208
231
  onGenerate();
232
+ } else {
233
+ state._tag = null
209
234
  }
235
+
210
236
  state.detailDrawer = false;
211
237
  showRouteDetail.value = false;
212
238
  }
@@ -219,6 +245,8 @@ const app = createApp({
219
245
  }
220
246
  state.detailDrawer = false;
221
247
  showRouteDetail.value = false;
248
+ state.focus = false
249
+ schemaCodeName.value = ''
222
250
  onGenerate();
223
251
  }
224
252
 
@@ -237,6 +265,30 @@ const app = createApp({
237
265
  onGenerate(false);
238
266
  }
239
267
 
268
+ function startDragDrawer(e) {
269
+ const startX = e.clientX;
270
+ const startWidth = state.drawerWidth;
271
+
272
+ function onMouseMove(moveEvent) {
273
+ const deltaX = startX - moveEvent.clientX;
274
+ const newWidth = Math.max(300, Math.min(800, startWidth + deltaX));
275
+ state.drawerWidth = newWidth;
276
+ }
277
+
278
+ function onMouseUp() {
279
+ document.removeEventListener('mousemove', onMouseMove);
280
+ document.removeEventListener('mouseup', onMouseUp);
281
+ document.body.style.cursor = '';
282
+ document.body.style.userSelect = '';
283
+ }
284
+
285
+ document.addEventListener('mousemove', onMouseMove);
286
+ document.addEventListener('mouseup', onMouseUp);
287
+ document.body.style.cursor = 'col-resize';
288
+ document.body.style.userSelect = 'none';
289
+ e.preventDefault();
290
+ }
291
+
240
292
  onMounted(async () => {
241
293
  await loadInitial();
242
294
  });
@@ -272,6 +324,8 @@ const app = createApp({
272
324
  showRenderGraph,
273
325
  renderCoreData,
274
326
  toggleShowField,
327
+ startDragDrawer,
328
+ onFocusChange
275
329
  };
276
330
  },
277
331
  });
@@ -7,7 +7,7 @@ import tests.service.schema as serv
7
7
 
8
8
  app = FastAPI(title="Demo API", description="A demo FastAPI application for router visualization")
9
9
 
10
- @app.get("/sprints", tags=['for-restapi'], response_model=list[serv.Sprint])
10
+ @app.get("/sprints", tags=['for-restapi', 'group_a'], response_model=list[serv.Sprint])
11
11
  def get_sprint():
12
12
  return []
13
13
 
@@ -96,4 +96,8 @@ def get_page_test_2():
96
96
 
97
97
  @app.get("/page_test_3/", tags=['for-page'], response_model=bool)
98
98
  def get_page_test_3_long_long_long_name():
99
+ return True
100
+
101
+ @app.get("/page_test_4/", tags=['for-page', 'group_b'])
102
+ def get_page_test_3_no_response_model():
99
103
  return True
@@ -2,3 +2,4 @@ from fastapi_voyager import create_voyager
2
2
  from tests.demo import app
3
3
 
4
4
  app.mount('/voyager', create_voyager(app, module_color={"tests.service": "red"}, module_prefix="tests.service"))
5
+ # app.mount('/voyager', create_voyager(app, module_color={"tests.service": "red"}))