fastapi-voyager 0.10.4__py3-none-any.whl → 0.11.1__py3-none-any.whl

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.
fastapi_voyager/filter.py CHANGED
@@ -192,4 +192,94 @@ def filter_subgraph_by_module_prefix(
192
192
 
193
193
  filtered_links = tag_route_links + merged_links + module_prefix_links
194
194
 
195
- return tags, routes, filtered_nodes, filtered_links
195
+ return tags, routes, filtered_nodes, filtered_links
196
+
197
+
198
+ def filter_subgraph_from_tag_to_schema_by_module_prefix(
199
+ *,
200
+ tags: list[Tag],
201
+ routes: list[Route],
202
+ links: list[Link],
203
+ nodes: list[SchemaNode],
204
+ module_prefix: str
205
+ ) -> Tuple[list[Tag], list[Route], list[SchemaNode], list[Link]]:
206
+ """Collapse schema graph so routes link directly to nodes whose module matches ``module_prefix``.
207
+
208
+ The routine keeps tag→route links untouched, prunes schema nodes whose module does not start
209
+ with ``module_prefix``, and merges the remaining schema relationships so each route connects
210
+ directly to the surviving schema nodes. Traversal stops once a qualifying node is reached and
211
+ guards against cycles in the schema graph.
212
+ """
213
+
214
+ if not module_prefix:
215
+ # empty prefix keeps existing graph structure, so simply reuse incoming data
216
+ return tags, routes, nodes, [lk for lk in links if lk.type in ("tag_route", "route_to_schema")]
217
+
218
+ route_links = [lk for lk in links if lk.type == "route_to_schema"]
219
+ schema_links = [lk for lk in links if lk.type in {"schema", "parent", "subset"}]
220
+ tag_route_links = [lk for lk in links if lk.type == "tag_route"]
221
+
222
+ node_lookup: dict[str, SchemaNode] = {node.id: node for node in (nodes + routes)}
223
+
224
+ filtered_nodes = [node for node in nodes if node_lookup[node.id].module.startswith(module_prefix)]
225
+ filtered_node_ids = {node.id for node in filtered_nodes}
226
+
227
+ adjacency: dict[str, list[str]] = {}
228
+ for link in (schema_links + route_links):
229
+ if link.source_origin not in node_lookup or link.target_origin not in node_lookup:
230
+ continue
231
+ adjacency.setdefault(link.source_origin, [])
232
+ if link.target_origin not in adjacency[link.source_origin]:
233
+ adjacency[link.source_origin].append(link.target_origin)
234
+
235
+ merged_links: list[Link] = []
236
+ seen_pairs: set[tuple[str, str]] = set()
237
+
238
+ for link in tag_route_links:
239
+ # print(link)
240
+ tag_id = link.source_origin
241
+ start_node_id = link.target_origin
242
+ if tag_id is None or start_node_id is None:
243
+ continue
244
+ if start_node_id not in node_lookup:
245
+ continue
246
+
247
+ visited: set[str] = set()
248
+ queue: deque[str] = deque([start_node_id])
249
+
250
+ while queue:
251
+ current = queue.popleft()
252
+ if current in visited:
253
+ continue
254
+ visited.add(current)
255
+
256
+ if current in filtered_node_ids:
257
+ key = (tag_id, current)
258
+ if key not in seen_pairs:
259
+ seen_pairs.add(key)
260
+ merged_links.append(
261
+ Link(
262
+ source=link.source,
263
+ source_origin=tag_id,
264
+ target=f"{current}::{PK}",
265
+ target_origin=current,
266
+ type="tag_to_schema",
267
+ )
268
+ )
269
+ # stop traversing past a qualifying node
270
+ continue
271
+
272
+ for next_node in adjacency.get(current, () ):
273
+ if next_node not in visited:
274
+ queue.append(next_node)
275
+
276
+ module_prefix_links = [
277
+ lk
278
+ for lk in links
279
+ if (lk.source_origin or "").startswith(module_prefix)
280
+ and (lk.target_origin or "").startswith(module_prefix)
281
+ ]
282
+
283
+ filtered_links = merged_links + module_prefix_links
284
+
285
+ return tags, [], filtered_nodes, filtered_links # route is skipped
fastapi_voyager/render.py CHANGED
@@ -37,7 +37,7 @@ class Renderer:
37
37
  fields_parts.append(field_str)
38
38
 
39
39
  header_color = 'tomato' if node.id == self.schema else '#009485'
40
- header = f"""<tr><td cellpadding="1.5" bgcolor="{header_color}" align="center" colspan="1" port="{PK}"> <font color="white"> {node.name} </font> </td> </tr>"""
40
+ header = f"""<tr><td cellpadding="6" bgcolor="{header_color}" align="center" colspan="1" port="{PK}"> <font color="white"> {node.name} </font></td> </tr>"""
41
41
  field_content = ''.join(fields_parts) if fields_parts else ''
42
42
  return f"""<<table border="1" cellborder="0" cellpadding="0" bgcolor="white"> {header} {field_content} </table>>"""
43
43
 
@@ -59,6 +59,8 @@ class Renderer:
59
59
  return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid, dashed", dir="back", minlen=3, taillabel = "< inherit >", color = "purple", tailport="n"];"""
60
60
  elif link.type == 'subset':
61
61
  return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid, dashed", dir="back", minlen=3, taillabel = "< subset >", color = "orange", tailport="n"];"""
62
+ elif link.type == 'tag_to_schema':
63
+ return f"""{h(link.source)}:e -> {h(link.target)}:w [style = "solid", dir="back", arrowtail="odot", minlen=3];"""
62
64
  else:
63
65
  raise ValueError(f'Unknown link type: {link.type}')
64
66
 
fastapi_voyager/server.py CHANGED
@@ -22,6 +22,7 @@ class OptionParam(BaseModel):
22
22
  dot: str
23
23
  enable_brief_mode: bool
24
24
  version: str
25
+ swagger_url: Optional[str] = None
25
26
 
26
27
  class Payload(BaseModel):
27
28
  tags: Optional[list[str]] = None
@@ -33,9 +34,11 @@ class Payload(BaseModel):
33
34
  brief: bool = False
34
35
  hide_primitive_route: bool = False
35
36
 
37
+
36
38
  def create_route(
37
39
  target_app: FastAPI,
38
40
  module_color: dict[str, str] | None = None,
41
+ swagger_url: Optional[str] = None,
39
42
  module_prefix: Optional[str] = None,
40
43
  ):
41
44
  """
@@ -56,7 +59,13 @@ def create_route(
56
59
  schemas = voyager.nodes[:]
57
60
  schemas.sort(key=lambda s: s.name)
58
61
 
59
- return OptionParam(tags=tags, schemas=schemas, dot=dot, enable_brief_mode=bool(module_prefix), version=__version__)
62
+ return OptionParam(
63
+ tags=tags,
64
+ schemas=schemas,
65
+ dot=dot,
66
+ enable_brief_mode=bool(module_prefix),
67
+ version=__version__,
68
+ swagger_url=swagger_url)
60
69
 
61
70
  @router.post("/dot", response_class=PlainTextResponse)
62
71
  def get_filtered_dot(payload: Payload) -> str:
@@ -71,7 +80,10 @@ def create_route(
71
80
  )
72
81
  voyager.analysis(target_app)
73
82
  if payload.brief:
74
- return voyager.render_brief_dot(module_prefix=module_prefix)
83
+ if payload.tags:
84
+ return voyager.render_tag_level_brief_dot(module_prefix=module_prefix)
85
+ else:
86
+ return voyager.render_overall_brief_dot(module_prefix=module_prefix)
75
87
  else:
76
88
  return voyager.render_dot()
77
89
 
@@ -196,8 +208,9 @@ def create_voyager(
196
208
  module_color: dict[str, str] | None = None,
197
209
  gzip_minimum_size: int | None = 500,
198
210
  module_prefix: Optional[str] = None,
211
+ swagger_url: Optional[str] = None,
199
212
  ) -> FastAPI:
200
- router = create_route(target_app, module_color=module_color, module_prefix=module_prefix)
213
+ router = create_route(target_app, module_color=module_color, module_prefix=module_prefix, swagger_url=swagger_url)
201
214
 
202
215
  app = FastAPI(title="fastapi-voyager demo server")
203
216
  if gzip_minimum_size is not None and gzip_minimum_size >= 0:
fastapi_voyager/type.py CHANGED
@@ -22,6 +22,7 @@ class Tag(NodeBase):
22
22
  @dataclass
23
23
  class Route(NodeBase):
24
24
  module: str
25
+ unique_id: str = ''
25
26
  response_schema: str = ''
26
27
  is_primitive: bool = True
27
28
 
@@ -51,7 +52,8 @@ class ModuleNode:
51
52
  # - subset: schema -> schema (subset)
52
53
  # - parent: schema -> schema (inheritance)
53
54
  # - schema: schema -> schema (field reference)
54
- LinkType = Literal['schema', 'parent', 'tag_route', 'subset', 'route_to_schema']
55
+ # - tag_to_schema: tag -> schema (only happens in module prefix filtering, aka brief mode)
56
+ LinkType = Literal['schema', 'parent', 'tag_route', 'subset', 'route_to_schema', 'tag_to_schema']
55
57
 
56
58
  @dataclass
57
59
  class Link:
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.10.4"
2
+ __version__ = "0.11.1"
@@ -6,15 +6,13 @@ from fastapi_voyager.type_helper import (
6
6
  get_bases_fields,
7
7
  is_inheritance_of_pydantic_base,
8
8
  get_pydantic_fields,
9
- get_vscode_link,
10
- get_source,
11
9
  get_type_name,
12
10
  update_forward_refs,
13
11
  is_non_pydantic_type
14
12
  )
15
13
  from pydantic import BaseModel
16
14
  from fastapi_voyager.type import Route, SchemaNode, Link, Tag, LinkType, FieldType, PK, CoreData
17
- from fastapi_voyager.filter import filter_graph, filter_subgraph_by_module_prefix
15
+ from fastapi_voyager.filter import filter_graph, filter_subgraph_from_tag_to_schema_by_module_prefix, filter_subgraph_by_module_prefix
18
16
  from fastapi_voyager.render import Renderer
19
17
  import pydantic_resolve.constant as const
20
18
 
@@ -123,6 +121,7 @@ class Voyager:
123
121
  id=route_id,
124
122
  name=route_name,
125
123
  module=route_module,
124
+ unique_id=route.unique_id,
126
125
  response_schema=get_type_name(route.response_model),
127
126
  is_primitive=is_primitive_response
128
127
  )
@@ -307,7 +306,8 @@ class Voyager:
307
306
  _tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
308
307
  return renderer.render_dot(_tags, _routes, _nodes, _links)
309
308
 
310
- def render_brief_dot(self, module_prefix: str | None = None):
309
+
310
+ def render_tag_level_brief_dot(self, module_prefix: str | None = None):
311
311
  _tags, _routes, _nodes, _links = filter_graph(
312
312
  schema=self.schema,
313
313
  schema_field=self.schema_field,
@@ -328,5 +328,29 @@ class Voyager:
328
328
 
329
329
  renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema)
330
330
 
331
+ _tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
332
+ return renderer.render_dot(_tags, _routes, _nodes, _links, True)
333
+
334
+ def render_overall_brief_dot(self, module_prefix: str | None = None):
335
+ _tags, _routes, _nodes, _links = filter_graph(
336
+ schema=self.schema,
337
+ schema_field=self.schema_field,
338
+ tags=self.tags,
339
+ routes=self.routes,
340
+ nodes=self.nodes,
341
+ links=self.links,
342
+ node_set=self.node_set,
343
+ )
344
+
345
+ _tags, _routes, _nodes, _links = filter_subgraph_from_tag_to_schema_by_module_prefix(
346
+ module_prefix=module_prefix,
347
+ tags=_tags,
348
+ routes=_routes,
349
+ nodes=_nodes,
350
+ links=_links,
351
+ )
352
+
353
+ renderer = Renderer(show_fields=self.show_fields, module_color=self.module_color, schema=self.schema)
354
+
331
355
  _tags, _routes, _links = self.handle_hide(_tags, _routes, _links)
332
356
  return renderer.render_dot(_tags, _routes, _nodes, _links, True)
@@ -42,11 +42,12 @@ export default defineComponent({
42
42
  async function loadSchemas() {
43
43
  // Use externally provided props.schemas dict directly; no network call.
44
44
  state.error = null;
45
- const dict = props.schemas && typeof props.schemas === "object" ? props.schemas : {};
45
+ const dict =
46
+ props.schemas && typeof props.schemas === "object" ? props.schemas : {};
46
47
  // Flatten to array for local operations
47
48
  state.schemas = Object.values(dict);
48
49
  state.schemaOptions = state.schemas.map((s) => ({
49
- label: `${s.name} (${s.id})`,
50
+ label: `${s.name} - ${s.id}`,
50
51
  value: s.id,
51
52
  }));
52
53
  // Maintain compatibility: loadingSchemas flag toggled quickly (no async work)
@@ -57,7 +58,7 @@ export default defineComponent({
57
58
  const needle = (val || "").toLowerCase();
58
59
  update(() => {
59
60
  let opts = state.schemas.map((s) => ({
60
- label: `${s.name} (${s.id})`,
61
+ label: `${s.name} - ${s.id}`,
61
62
  value: s.id,
62
63
  }));
63
64
  if (needle) {
@@ -127,7 +128,6 @@ export default defineComponent({
127
128
  }
128
129
  });
129
130
 
130
-
131
131
  function close() {
132
132
  emit("close");
133
133
  }
@@ -178,13 +178,12 @@ export default defineComponent({
178
178
  :disable="!state.schemaFullname"
179
179
  :loading="state.querying"
180
180
  @click="onQuery" />
181
- </div>
182
181
  <q-btn
183
182
  flat dense round icon="close"
184
183
  aria-label="Close"
185
184
  @click="close"
186
- style="position:absolute; top:6px; right:6px; z-index:11; background:rgba(255,255,255,0.85);"
187
185
  />
186
+ </div>
188
187
  <div v-if="state.error" style="position:absolute; top:52px; left:8px; z-index:10; color:#c10015; font-size:12px;">{{ state.error }}</div>
189
188
  <div id="graph-schema-field" style="width:100%; height:100%; overflow:auto; background:#fafafa"></div>
190
189
  </div>
@@ -57,6 +57,13 @@ export class GraphUI {
57
57
  return $result;
58
58
  }
59
59
 
60
+ highlightSchemaBanner(node) {
61
+ const ele = node.querySelector("polygon[fill='#009485']")
62
+ if (ele) {
63
+ ele.setAttribute('fill', 'tomato');
64
+ }
65
+ }
66
+
60
67
  _init() {
61
68
  const self = this;
62
69
  $(this.selector).graphviz({
@@ -65,6 +72,26 @@ export class GraphUI {
65
72
  ready: function () {
66
73
  self.gv = this;
67
74
 
75
+ self.gv.nodes().dblclick(function (event) {
76
+ event.stopPropagation();
77
+ try {
78
+ self.highlightSchemaBanner(this)
79
+ } catch(e) {
80
+ console.log(e)
81
+ }
82
+ const set = $();
83
+ set.push(this);
84
+ // const obj = { set, direction: "bidirectional" };
85
+ const schemaName = event.currentTarget.dataset.name;
86
+ // self.currentSelection = [obj];
87
+ if (schemaName) {
88
+ try {
89
+ self.options.onSchemaClick(schemaName);
90
+ } catch (e) {
91
+ console.warn("onSchemaClick callback failed", e);
92
+ }
93
+ }
94
+ });
68
95
  self.gv.nodes().click(function (event) {
69
96
  const set = $();
70
97
  set.push(this);
@@ -83,12 +110,10 @@ export class GraphUI {
83
110
  } else {
84
111
  self.currentSelection = [obj];
85
112
  self._highlight();
86
- if (schemaName) {
87
- try {
88
- self.options.onSchemaClick(schemaName);
89
- } catch (e) {
90
- console.warn("onSchemaClick callback failed", e);
91
- }
113
+ try {
114
+ self.options.resetCb();
115
+ } catch (e) {
116
+ console.warn("resetCb callback failed", e);
92
117
  }
93
118
  }
94
119
  });
@@ -107,28 +132,34 @@ export class GraphUI {
107
132
 
108
133
  // svg 背景点击高亮清空
109
134
 
110
- $(document).off('click.graphui').on('click.graphui', function (evt) {
111
- // 如果点击目标不在 graph 容器内,直接退出
112
- const graphContainer = $(self.selector)[0];
113
- if (!graphContainer || !evt.target || !graphContainer.contains(evt.target)) {
114
- return;
115
- }
135
+ $(document)
136
+ .off("click.graphui")
137
+ .on("click.graphui", function (evt) {
138
+ // 如果点击目标不在 graph 容器内,直接退出
139
+ const graphContainer = $(self.selector)[0];
140
+ if (
141
+ !graphContainer ||
142
+ !evt.target ||
143
+ !graphContainer.contains(evt.target)
144
+ ) {
145
+ return;
146
+ }
116
147
 
117
- let isNode = false;
118
- const $nodes = self.gv.nodes();
119
- const node = evt.target.parentNode;
120
- $nodes.each(function () {
121
- if (this === node) {
122
- isNode = true;
148
+ let isNode = false;
149
+ const $nodes = self.gv.nodes();
150
+ const node = evt.target.parentNode;
151
+ $nodes.each(function () {
152
+ if (this === node) {
153
+ isNode = true;
154
+ }
155
+ });
156
+ if (!isNode && self.gv) {
157
+ self.gv.highlight();
158
+ if (self.options.resetCb) {
159
+ self.options.resetCb();
160
+ }
123
161
  }
124
162
  });
125
- if (!isNode && self.gv) {
126
- self.gv.highlight();
127
- if (self.options.resetCb) {
128
- self.options.resetCb();
129
- }
130
- }
131
- });
132
163
 
133
164
  $(document).on("keydown.graphui", function (evt) {
134
165
  if (evt.keyCode === 27 && self.gv) {
@@ -127,12 +127,10 @@
127
127
  <ul>
128
128
  <li>scroll to zoom in/out</li>
129
129
  <li>
130
- click node to check highlight related nodes on the
131
- chart, esc to unselect
130
+ double click node to view details.
132
131
  </li>
133
132
  <li>
134
- shift + click to see schema's dependencies without
135
- unrelated nodes
133
+ shift + click to see schema's dependencies without unrelated nodes.
136
134
  </li>
137
135
  </ul>
138
136
  </div>
@@ -149,7 +147,6 @@
149
147
  :width="state.drawerWidth"
150
148
  side="right"
151
149
  style="border-left: 1px solid #888;"
152
- overlay
153
150
  bordered
154
151
  >
155
152
  <!-- 可拖拽的调整栏 -->
@@ -221,6 +218,9 @@
221
218
  :name="state.tag == tag.name ? 'folder' : 'folder_open'"
222
219
  ></q-icon>
223
220
  <span>{{ tag.name }} <q-chip style="position:relative; top: -1px;" class="q-ml-sm" dense>{{ tag.routes.length }}</q-chip></span>
221
+ <a target="_blank" class="q-ml-sm" v-if="state.swaggerUrl" :href="state.swaggerUrl + '#/' + tag.name">
222
+ <q-icon size="small" name="link"></q-icon>
223
+ </a>
224
224
  </div>
225
225
  </template>
226
226
  <q-list separator style="overflow: auto; max-height: 60vh;">
@@ -240,6 +240,9 @@
240
240
  name="data_object"
241
241
  ></q-icon>
242
242
  {{ route.name }}
243
+ <a target="_blank" class="q-ml-sm" v-if="state.swaggerUrl" :href="state.swaggerUrl + '#/' + tag.name + '/' + route.unique_id">
244
+ <q-icon size="small" name="link"></q-icon>
245
+ </a>
243
246
  </span>
244
247
  </q-item-section>
245
248
  </q-item>
@@ -24,6 +24,7 @@ const app = createApp({
24
24
  focus: false,
25
25
  hidePrimitiveRoute: false,
26
26
  generating: false,
27
+ swaggerUrl: null,
27
28
  rawTags: [], // [{ name, routes: [{ id, name }] }]
28
29
  rawSchemas: new Set(), // [{ name, id }]
29
30
  rawSchemasFull: {}, // full schemas dict: { [schema.id]: schema }
@@ -48,6 +49,7 @@ const app = createApp({
48
49
  const schemaCodeName = ref("");
49
50
  const routeCodeId = ref("");
50
51
  const showRouteDetail = ref(false);
52
+ let graphUI = null;
51
53
 
52
54
  function openDetail() {
53
55
  showDetail.value = true;
@@ -77,6 +79,7 @@ const app = createApp({
77
79
  }, {});
78
80
  state.enableBriefMode = data.enable_brief_mode || false;
79
81
  state.version = data.version || "";
82
+ state.swaggerUrl = data.swagger_url || null
80
83
 
81
84
  // default route options placeholder
82
85
  } catch (e) {
@@ -88,13 +91,13 @@ const app = createApp({
88
91
 
89
92
  async function onFocusChange(val) {
90
93
  if (val) {
91
- await onGenerate(false)
94
+ await onGenerate(true); // target could be out of view when switchingfrom big to small
92
95
  } else {
93
- await onGenerate(false)
96
+ await onGenerate(false);
94
97
  setTimeout(() => {
95
- const ele = $(`[data-name='${schemaCodeName.value}'] polygon`)
96
- ele.click()
97
- }, 1)
98
+ const ele = $(`[data-name='${schemaCodeName.value}'] polygon`);
99
+ ele.dblclick();
100
+ }, 1);
98
101
  }
99
102
  }
100
103
 
@@ -118,30 +121,31 @@ const app = createApp({
118
121
  const dotText = await res.text();
119
122
 
120
123
  // create graph instance once
121
- const graphUI = new GraphUI("#graph", {
122
- onSchemaShiftClick: (id) => {
123
- if (state.rawSchemas.has(id)) {
124
- resetDetailPanels()
125
- schemaFieldFilterSchema.value = id;
126
- showSchemaFieldFilter.value = true;
127
- }
128
- },
129
- onSchemaClick: (id) => {
130
- resetDetailPanels()
131
- if (state.rawSchemas.has(id)) {
132
- schemaCodeName.value = id;
133
- state.detailDrawer = true;
134
- }
135
- if (id in state.routeItems) {
136
- routeCodeId.value = id;
137
- showRouteDetail.value = true;
138
- }
139
- },
140
- resetCb: () => {
141
- resetDetailPanels()
142
- }
143
- });
144
-
124
+ if (!graphUI) {
125
+ graphUI = new GraphUI("#graph", {
126
+ onSchemaShiftClick: (id) => {
127
+ if (state.rawSchemas.has(id)) {
128
+ resetDetailPanels();
129
+ schemaFieldFilterSchema.value = id;
130
+ showSchemaFieldFilter.value = true;
131
+ }
132
+ },
133
+ onSchemaClick: (id) => {
134
+ resetDetailPanels();
135
+ if (state.rawSchemas.has(id)) {
136
+ schemaCodeName.value = id;
137
+ state.detailDrawer = true;
138
+ }
139
+ if (id in state.routeItems) {
140
+ routeCodeId.value = id;
141
+ showRouteDetail.value = true;
142
+ }
143
+ },
144
+ resetCb: () => {
145
+ resetDetailPanels();
146
+ },
147
+ });
148
+ }
145
149
  await graphUI.render(dotText, resetZoom);
146
150
  } catch (e) {
147
151
  console.error("Generate failed", e);
@@ -213,9 +217,9 @@ const app = createApp({
213
217
  }
214
218
 
215
219
  function resetDetailPanels() {
216
- state.detailDrawer = false
220
+ state.detailDrawer = false;
217
221
  showRouteDetail.value = false;
218
- schemaCodeName.value = ''
222
+ schemaCodeName.value = "";
219
223
  }
220
224
 
221
225
  async function onReset() {
@@ -223,9 +227,8 @@ const app = createApp({
223
227
  state.routeId = "";
224
228
  state.schemaId = null;
225
229
  // state.showFields = "object";
226
- state.brief = false;
227
- state.focus = false
228
- schemaCodeName.value = ''
230
+ state.focus = false;
231
+ schemaCodeName.value = "";
229
232
  onGenerate();
230
233
  }
231
234
 
@@ -234,11 +237,11 @@ const app = createApp({
234
237
  state._tag = tagName;
235
238
  state.tag = tagName;
236
239
  state.routeId = "";
237
- state.focus = false
238
- schemaCodeName.value = ''
240
+ state.focus = false;
241
+ schemaCodeName.value = "";
239
242
  onGenerate();
240
243
  } else {
241
- state._tag = null
244
+ state._tag = null;
242
245
  }
243
246
 
244
247
  state.detailDrawer = false;
@@ -253,8 +256,8 @@ const app = createApp({
253
256
  }
254
257
  state.detailDrawer = false;
255
258
  showRouteDetail.value = false;
256
- state.focus = false
257
- schemaCodeName.value = ''
259
+ state.focus = false;
260
+ schemaCodeName.value = "";
258
261
  onGenerate();
259
262
  }
260
263
 
@@ -276,24 +279,24 @@ const app = createApp({
276
279
  function startDragDrawer(e) {
277
280
  const startX = e.clientX;
278
281
  const startWidth = state.drawerWidth;
279
-
282
+
280
283
  function onMouseMove(moveEvent) {
281
- const deltaX = startX - moveEvent.clientX;
284
+ const deltaX = startX - moveEvent.clientX;
282
285
  const newWidth = Math.max(300, Math.min(800, startWidth + deltaX));
283
286
  state.drawerWidth = newWidth;
284
287
  }
285
-
288
+
286
289
  function onMouseUp() {
287
- document.removeEventListener('mousemove', onMouseMove);
288
- document.removeEventListener('mouseup', onMouseUp);
289
- document.body.style.cursor = '';
290
- document.body.style.userSelect = '';
290
+ document.removeEventListener("mousemove", onMouseMove);
291
+ document.removeEventListener("mouseup", onMouseUp);
292
+ document.body.style.cursor = "";
293
+ document.body.style.userSelect = "";
291
294
  }
292
-
293
- document.addEventListener('mousemove', onMouseMove);
294
- document.addEventListener('mouseup', onMouseUp);
295
- document.body.style.cursor = 'col-resize';
296
- document.body.style.userSelect = 'none';
295
+
296
+ document.addEventListener("mousemove", onMouseMove);
297
+ document.addEventListener("mouseup", onMouseUp);
298
+ document.body.style.cursor = "col-resize";
299
+ document.body.style.userSelect = "none";
297
300
  e.preventDefault();
298
301
  }
299
302
 
@@ -333,7 +336,7 @@ const app = createApp({
333
336
  renderCoreData,
334
337
  toggleShowField,
335
338
  startDragDrawer,
336
- onFocusChange
339
+ onFocusChange,
337
340
  };
338
341
  },
339
342
  });
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.10.4
3
+ Version: 0.11.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
@@ -244,20 +244,28 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
244
244
  - [x] fix focus in brief-mode
245
245
  - [x] ui: adjust focus position
246
246
  - [x] refactor naming
247
+ - [x] fix layout issue when rendering huge graph
247
248
  - 0.10.4
248
249
  - [x] fix: when focus is on, should ensure changes from other params not broken.
250
+ - 0.10.5
251
+ - [x] double click to show details, and highlight as tomato
249
252
 
250
253
 
251
254
  #### 0.11
252
- - [ ] enable/disable module cluster (to save space)
253
- - [ ] fix layout issue when rendering huge graph
254
- - [ ] logging information
255
- - [ ] support opening route in swagger
256
- - config docs path
257
- - [ ] add tests
258
- - [ ] fix: focus should reset zoom
255
+ - 0.11.1
256
+ - [x] support opening route in swagger
257
+ - [x] config docs path
258
+ - [x] provide option to hide routes in brief mode (auto hide in full graph mode)
259
+ - 0.11.2
260
+ - [ ] enable/disable module cluster (to save space)
261
+ - [ ] logging information
262
+ - [ ] sort field name
263
+ - [ ] set max limit for fields
264
+ - [ ] add info icon alone with schema node
265
+ - [ ] add loading for field detail panel
259
266
 
260
267
  #### 0.12
268
+ - [ ] add tests
261
269
  - [ ] integration with pydantic-resolve
262
270
  - [ ] show hint for resolve, post fields
263
271
  - [ ] display loader as edges
@@ -1,24 +1,24 @@
1
1
  fastapi_voyager/__init__.py,sha256=tZy0Nkj8kTaMgbvHy-mGxVcFGVX0Km-36dnzsAIG2uk,230
2
2
  fastapi_voyager/cli.py,sha256=kQb4g6JEGZR99e5r8LyFFEeb_-uT-n_gp_sDoYG3R7k,11118
3
- fastapi_voyager/filter.py,sha256=C718iwMjnnFXtqZISe9tzDRLAaY2FjbcE2LgkauEwnw,8095
3
+ fastapi_voyager/filter.py,sha256=GY2J9Vfsf_wbFwC-0t74-Lf-OlO77PnhEXD_rmgkfSw,11574
4
4
  fastapi_voyager/module.py,sha256=Z2QHNmiLk6ZAJlm2nSmO875Q33TweSg8UxZSzIpU9zY,3499
5
- fastapi_voyager/render.py,sha256=sR1oUh8d3hhDalQhUcrL-k2ZYAWbLDGpGDOB0dDaqRQ,7804
6
- fastapi_voyager/server.py,sha256=6kCj906N4PVpKbUI8eq7bJ4RoET1kIQgUQUf-oMFdSY,6326
7
- fastapi_voyager/type.py,sha256=pWYKmgb9e0W_JeD7k54Mr2lxUZV_Ir9TNpewGRwHyHQ,1629
5
+ fastapi_voyager/render.py,sha256=EtSkQWb3k3g6T6pnDW2GF9BM-NkDkbDjhlsnF0u-IG0,7970
6
+ fastapi_voyager/server.py,sha256=UkTeMFduq4Ebt6eNisfPVUiy04XzHmOaZkBuwEk5mus,6617
7
+ fastapi_voyager/type.py,sha256=VmcTB1G-LOT70EWCzi4LU_FUkSGWUIBJX15T_J5HnOo,1764
8
8
  fastapi_voyager/type_helper.py,sha256=hjBC4E0tgBpQDlYxGg74uK07SXjsrAgictEETJfIpYM,9231
9
- fastapi_voyager/version.py,sha256=72VoP-5nPbuiOf_uhKc4wtpmuLClUnAUOlkT8SDgWdU,49
10
- fastapi_voyager/voyager.py,sha256=r-_V0EPWwBM7N4hMlo-Si48mHVDc3zc9oGgTPCoEW_8,12377
11
- fastapi_voyager/web/graph-ui.js,sha256=FmKA3eeHMEeUDNuay3f54_fv4eGHT7MT0TaGqXhyMWQ,4978
9
+ fastapi_voyager/version.py,sha256=mqCYzIn6okf6Z1_ZJPQL0K0ySha6En_onGIReGtmFxM,49
10
+ fastapi_voyager/voyager.py,sha256=etSIrqJJQXArxYLp_45wa0kKdqSebYy9TBYbuyK0iC4,13332
11
+ fastapi_voyager/web/graph-ui.js,sha256=DTedkpZNbtufexONVkJ8mOwF_-VnvxoReYHtox6IKR4,5842
12
12
  fastapi_voyager/web/graphviz.svg.css,sha256=zDCjjpT0Idufu5YOiZI76PL70-avP3vTyzGPh9M85Do,1563
13
13
  fastapi_voyager/web/graphviz.svg.js,sha256=lvAdbjHc-lMSk4GQp-iqYA2PCFX4RKnW7dFaoe0LUHs,16005
14
- fastapi_voyager/web/index.html,sha256=ItGJUnItnG_e11uXW_eKAWK5LxCAphXyB0oh8mOOCVw,16911
14
+ fastapi_voyager/web/index.html,sha256=1WkNQ_xuS-oy4WkRChk53b9mZjATC5XZbIhPZ_pVBJQ,17317
15
15
  fastapi_voyager/web/quasar.min.css,sha256=F5jQe7X2XT54VlvAaa2V3GsBFdVD-vxDZeaPLf6U9CU,203145
16
16
  fastapi_voyager/web/quasar.min.js,sha256=h0ftyPMW_CRiyzeVfQqiup0vrVt4_QWojpqmpnpn07E,502974
17
- fastapi_voyager/web/vue-main.js,sha256=BPGnowdtFqba8vPMPV2eMwXihecPSr_BVsIcjS5jOXg,10272
17
+ fastapi_voyager/web/vue-main.js,sha256=ZM1BmFcA9hz6FmP5W-J1xxNE8mE_Rsj-5-dlh43zfcQ,10483
18
18
  fastapi_voyager/web/component/render-graph.js,sha256=e8Xgh2Kl-nYU0P1gstEmAepCgFnk2J6UdxW8TlMafGs,2322
19
19
  fastapi_voyager/web/component/route-code-display.js,sha256=8NJPPjNRUC21gjpY8XYEQs4RBbhX1pCiqEhJp39ku6k,3678
20
20
  fastapi_voyager/web/component/schema-code-display.js,sha256=UgFotzvqSuhnPXNOr6w_r1fV2_savRiCdokEvferutE,6244
21
- fastapi_voyager/web/component/schema-field-filter.js,sha256=PR8d2_u1UiSLrS5zqAvY2UR8LbvjBqiUDt71tm1DJTs,6345
21
+ fastapi_voyager/web/component/schema-field-filter.js,sha256=c--XiXJrhIS7sYo1x8ZwMoqak0k9xLkNYTWoli-zd38,6253
22
22
  fastapi_voyager/web/icon/android-chrome-192x192.png,sha256=35sBy6jmUFJCcquStaafHH1qClZIbd-X3PIKSeLkrNo,37285
23
23
  fastapi_voyager/web/icon/android-chrome-512x512.png,sha256=eb2eDjCwIruc05029_0L9hcrkVkv8KceLn1DJMYU0zY,210789
24
24
  fastapi_voyager/web/icon/apple-touch-icon.png,sha256=gnWK46tPnvSw1-oYZjgI02wpoO4OrIzsVzGHC5oKWO0,33187
@@ -26,8 +26,8 @@ fastapi_voyager/web/icon/favicon-16x16.png,sha256=JC07jEzfIYxBIoQn_FHXvyHuxESdhW
26
26
  fastapi_voyager/web/icon/favicon-32x32.png,sha256=C7v1h58cfWOsiLp9yOIZtlx-dLasBcq3NqpHVGRmpt4,1859
27
27
  fastapi_voyager/web/icon/favicon.ico,sha256=tZolYIXkkBcFiYl1A8ksaXN2VjGamzcSdes838dLvNc,15406
28
28
  fastapi_voyager/web/icon/site.webmanifest,sha256=ep4Hzh9zhmiZF2At3Fp1dQrYQuYF_3ZPZxc1KcGBvwQ,263
29
- fastapi_voyager-0.10.4.dist-info/METADATA,sha256=vYGgXTEEV6BSyO0iv3u5Z08NIqOaXTlWkU9nd7ZMKMs,9449
30
- fastapi_voyager-0.10.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
- fastapi_voyager-0.10.4.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
32
- fastapi_voyager-0.10.4.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
33
- fastapi_voyager-0.10.4.dist-info/RECORD,,
29
+ fastapi_voyager-0.11.1.dist-info/METADATA,sha256=K47oTptR1SQCs7hgvlhJSX9FeDO0knh-QFtgjjfe8mY,9767
30
+ fastapi_voyager-0.11.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
+ fastapi_voyager-0.11.1.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
32
+ fastapi_voyager-0.11.1.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
33
+ fastapi_voyager-0.11.1.dist-info/RECORD,,