fastapi-voyager 0.12.5__py3-none-any.whl → 0.12.7__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/server.py CHANGED
@@ -34,6 +34,8 @@ def _build_ga_snippet(ga_id: str | None) -> str:
34
34
 
35
35
  INITIAL_PAGE_POLICY = Literal['first', 'full', 'empty']
36
36
 
37
+ # ---------- setup ----------
38
+
37
39
  class OptionParam(BaseModel):
38
40
  tags: list[Tag]
39
41
  schemas: list[SchemaNode]
@@ -49,11 +51,21 @@ class Payload(BaseModel):
49
51
  schema_field: str | None = None
50
52
  route_name: str | None = None
51
53
  show_fields: str = 'object'
52
- show_meta: bool = False
53
54
  brief: bool = False
54
55
  hide_primitive_route: bool = False
55
56
  show_module: bool = True
56
57
 
58
+ # ---------- search ----------
59
+ class SearchResultOptionParam(BaseModel):
60
+ tags: list[Tag]
61
+
62
+ class SchemaSearchPayload(BaseModel): # leave tag, route out
63
+ schema_name: str | None = None
64
+ schema_field: str | None = None
65
+ show_fields: str = 'object'
66
+ brief: bool = False
67
+ hide_primitive_route: bool = False
68
+ show_module: bool = True
57
69
 
58
70
  def create_voyager(
59
71
  target_app: FastAPI,
@@ -91,6 +103,25 @@ def create_voyager(
91
103
  swagger_url=swagger_url,
92
104
  initial_page_policy=initial_page_policy)
93
105
 
106
+ @router.post("/dot-search", response_model=SearchResultOptionParam)
107
+ def get_search_dot(payload: SchemaSearchPayload):
108
+ voyager = Voyager(
109
+ schema=payload.schema_name,
110
+ schema_field=payload.schema_field,
111
+ show_fields=payload.show_fields,
112
+ module_color=module_color,
113
+ hide_primitive_route=payload.hide_primitive_route,
114
+ show_module=payload.show_module,
115
+ )
116
+ voyager.analysis(target_app)
117
+ tags = voyager.calculate_filtered_tag_and_route()
118
+
119
+ for t in tags:
120
+ t.routes.sort(key=lambda r: r.name)
121
+ tags.sort(key=lambda t: t.name)
122
+
123
+ return SearchResultOptionParam(tags=tags)
124
+
94
125
  @router.post("/dot", response_class=PlainTextResponse)
95
126
  def get_filtered_dot(payload: Payload) -> str:
96
127
  voyager = Voyager(
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.12.5"
2
+ __version__ = "0.12.7"
@@ -297,6 +297,22 @@ class Voyager:
297
297
  else:
298
298
  return tags, routes, links
299
299
 
300
+ def calculate_filtered_tag_and_route(self):
301
+ _tags, _routes, _, _ = filter_graph(
302
+ schema=self.schema,
303
+ schema_field=self.schema_field,
304
+ tags=self.tags,
305
+ routes=self.routes,
306
+ nodes=self.nodes,
307
+ links=self.links,
308
+ node_set=self.node_set,
309
+ )
310
+ # filter tag.routes based by _routes
311
+ route_ids = {r.id for r in _routes}
312
+ for t in _tags:
313
+ t.routes = [r for r in t.routes if r.id in route_ids]
314
+ return _tags
315
+
300
316
  def render_dot(self):
301
317
  _tags, _routes, _nodes, _links = filter_graph(
302
318
  schema=self.schema,
@@ -9,14 +9,31 @@ export class GraphUI {
9
9
  this._init();
10
10
  }
11
11
 
12
- _highlight() {
12
+ _highlight(mode = "bidirectional") {
13
13
  let highlightedNodes = $();
14
14
  for (const selection of this.currentSelection) {
15
- const nodes = this._getAffectedNodes(selection.set, "bidirectional");
15
+ const nodes = this._getAffectedNodes(selection.set, mode);
16
16
  highlightedNodes = highlightedNodes.add(nodes);
17
17
  }
18
18
  if (this.gv) {
19
19
  this.gv.highlight(highlightedNodes, true);
20
+ this.gv.bringToFront(highlightedNodes);
21
+ }
22
+ }
23
+
24
+ _highlightEdgeNodes() {
25
+ let highlightedNodes = $();
26
+ const [up, down, edge] = this.currentSelection
27
+ highlightedNodes = highlightedNodes.add(
28
+ this._getAffectedNodes(up.set, up.direction)
29
+ );
30
+ highlightedNodes = highlightedNodes.add(
31
+ this._getAffectedNodes(down.set, down.direction)
32
+ );
33
+ highlightedNodes = highlightedNodes.add(edge.set)
34
+ if (this.gv) {
35
+ this.gv.highlight(highlightedNodes, true);
36
+ this.gv.bringToFront(highlightedNodes);
20
37
  }
21
38
  }
22
39
 
@@ -65,6 +82,7 @@ export class GraphUI {
65
82
  }
66
83
  }
67
84
 
85
+
68
86
  _init() {
69
87
  const self = this;
70
88
  $(this.selector).graphviz({
@@ -93,13 +111,20 @@ export class GraphUI {
93
111
  });
94
112
 
95
113
  self.gv.edges().click(function (event) {
96
- // const set = $();
97
- // const downStreamNode = event.currentTarget.dataset.name.split("->")[1];
98
- // const nodes = self.gv.nodesByName();
99
- // set.push(nodes[downStreamNode]);
100
- // const obj = { set, direction: "single" };
101
- // self.currentSelection = [obj];
102
- // todo highlight edge and downstream node
114
+ const up = $();
115
+ const down = $();
116
+ const edge = $();
117
+ const [upStreamNode, downStreamNode] = event.currentTarget.dataset.name.split("->");
118
+ const nodes = self.gv.nodesByName();
119
+ up.push(nodes[upStreamNode]);
120
+ down.push(nodes[downStreamNode]);
121
+ edge.push(this)
122
+ const upObj = { set: up, direction: "upstream" };
123
+ const downObj = { set: down, direction: "downstream" };
124
+ const edgeOjb = { set: edge, direction: "single"};
125
+ self.currentSelection = [upObj, downObj, edgeOjb];
126
+
127
+ self._highlightEdgeNodes();
103
128
  })
104
129
 
105
130
  self.gv.nodes().click(function (event) {
@@ -127,15 +152,6 @@ export class GraphUI {
127
152
  }
128
153
  });
129
154
 
130
- self.gv.clusters().click(function (event) {
131
- const set = $();
132
- set.push(this);
133
- const obj = { set, direction: "single" };
134
- self.currentSelection = [obj];
135
- self._highlight();
136
- });
137
-
138
- // click background to reset highlight
139
155
  $(document)
140
156
  .off("click.graphui")
141
157
  .on("click.graphui", function (evt) {
@@ -150,9 +166,9 @@ export class GraphUI {
150
166
  }
151
167
 
152
168
  let isNode = false;
153
- const $nodes = self.gv.nodes();
169
+ const $everything = self.gv.$nodes.add(self.gv.$edges).add(self.gv.$clusters);
154
170
  const node = evt.target.parentNode;
155
- $nodes.each(function () {
171
+ $everything.each(function () {
156
172
  if (this === node) {
157
173
  isNode = true;
158
174
  }
@@ -21,6 +21,8 @@
21
21
  url: null,
22
22
  svg: null,
23
23
  shrink: "0.125pt",
24
+ edgeHitPadding: 12,
25
+ pointerCursor: true,
24
26
  tooltips: {
25
27
  init: function ($graph) {
26
28
  var $a = $(this);
@@ -168,9 +170,20 @@
168
170
  var that = this;
169
171
  var options = this.options;
170
172
 
173
+ if (type === "edge" && options.edgeHitPadding) {
174
+ this.ensureEdgeHitArea($el, options.edgeHitPadding);
175
+ }
176
+
177
+ if (options.pointerCursor && (type === "edge" || type === "node")) {
178
+ this.setInteractiveCursor($el, type === "edge");
179
+ }
180
+
171
181
  // save the colors of the paths, ellipses and polygons
172
182
  $el.find("polygon, ellipse, path").each(function () {
173
183
  var $this = $(this);
184
+ if ($this.attr("data-graphviz-hitbox") === "true") {
185
+ return;
186
+ }
174
187
  // save original colors
175
188
  $this.data("graphviz.svg.color", {
176
189
  fill: $this.attr("fill"),
@@ -314,6 +327,69 @@
314
327
  }
315
328
  };
316
329
 
330
+ GraphvizSvg.prototype.ensureEdgeHitArea = function ($edge, padding) {
331
+ var width = parseFloat(padding);
332
+ if (!isFinite(width) || width <= 0) {
333
+ return;
334
+ }
335
+ var $paths = $edge
336
+ .children("path")
337
+ .filter(function () {
338
+ return $(this).attr("data-graphviz-hitbox") !== "true";
339
+ });
340
+ if (!$paths.length) {
341
+ return;
342
+ }
343
+ $paths.each(function () {
344
+ var $path = $(this);
345
+ var $existing = $path.prev('[data-graphviz-hitbox="true"]');
346
+ if ($existing.length) {
347
+ $existing.attr("stroke-width", width);
348
+ return;
349
+ }
350
+ var clone = this.cloneNode(false);
351
+
352
+ /**
353
+ * gtp-5-codex:
354
+ * Cloning the edge paths without copying D3’s data binding caused those Cannot
355
+ * read properties of undefined (reading 'key') errors when d3-graphviz re-rendered.
356
+ * I now copy the original path’s bound datum (__data__) onto the transparent hitbox
357
+ * clone inside ensureEdgeHitArea, so D3 still finds the expected metadata.
358
+ */
359
+ if (this.__data__) {
360
+ clone.__data__ = this.__data__;
361
+ }
362
+
363
+ var $clone = $(clone);
364
+ $clone.attr({
365
+ "data-graphviz-hitbox": "true",
366
+ stroke: "transparent",
367
+ fill: "none",
368
+ "stroke-width": width,
369
+ });
370
+ $clone.attr("pointer-events", "stroke");
371
+ $clone.css("pointer-events", "stroke");
372
+ if (!$clone.attr("stroke-linecap")) {
373
+ $clone.attr("stroke-linecap", $path.attr("stroke-linecap") || "round");
374
+ }
375
+ $clone.insertBefore($path);
376
+ });
377
+ };
378
+
379
+ GraphvizSvg.prototype.setInteractiveCursor = function ($el, isEdge) {
380
+ $el.css("cursor", "pointer");
381
+ var selectors = "path, polygon, ellipse, rect, text";
382
+ $el.find(selectors).each(function () {
383
+ $(this).css("cursor", "pointer");
384
+ });
385
+ if (isEdge) {
386
+ $el.children('[data-graphviz-hitbox="true"]').css("cursor", "pointer");
387
+ }
388
+ $el.find("a").each(function () {
389
+ $(this).css("cursor", "pointer");
390
+ });
391
+ };
392
+
317
393
  GraphvizSvg.prototype.convertToPx = function (val) {
318
394
  var retval = val;
319
395
  if (typeof val == "string") {
@@ -372,6 +448,9 @@
372
448
  var bg = this.$element.css("background");
373
449
  $el.find("polygon, ellipse, path").each(function () {
374
450
  var $this = $(this);
451
+ if ($this.attr("data-graphviz-hitbox") === "true") {
452
+ return;
453
+ }
375
454
  var color = $this.data("graphviz.svg.color");
376
455
  if (color.fill && color.fill != "none") {
377
456
  $this.attr("fill", getColor(color.fill, bg)); // don't set fill if it's a path
@@ -386,6 +465,9 @@
386
465
  GraphvizSvg.prototype.restoreElement = function ($el) {
387
466
  $el.find("polygon, ellipse, path").each(function () {
388
467
  var $this = $(this);
468
+ if ($this.attr("data-graphviz-hitbox") === "true") {
469
+ return;
470
+ }
389
471
  var color = $this.data("graphviz.svg.color");
390
472
  if (color.fill && color.fill != "none") {
391
473
  $this.attr("fill", color.fill); // don't set fill if it's a path
@@ -120,24 +120,56 @@
120
120
  />
121
121
  </div>
122
122
  <div class="col-auto q-ml-auto">
123
+ </div>
124
+ <div class="col-auto row items-center q-gutter-sm">
125
+ <q-select
126
+ dense
127
+ outlined
128
+ use-input
129
+ fill-input
130
+ hide-selected
131
+ input-debounce="200"
132
+ v-model="store.state.search.schemaName"
133
+ :options="store.state.search.schemaOptions"
134
+ option-label="label"
135
+ option-value="value"
136
+ emit-value
137
+ map-options
138
+ style="min-width: 320px;"
139
+ clearable
140
+ label="Select schema"
141
+ @update:model-value="onSearchSchemaChange"
142
+ @filter="filterSearchSchemas"
143
+ @clear="resetSearch"
144
+ ></q-select>
145
+
146
+ <q-select
147
+ dense
148
+ outlined
149
+ v-model="store.state.search.fieldName"
150
+ :disable="!store.state.search.schemaName || store.state.search.fieldOptions.length===0"
151
+ :options="store.state.search.fieldOptions"
152
+ style="min-width: 180px"
153
+ clearable
154
+ label="Select field (optional)"
155
+ ></q-select>
123
156
  <q-btn
124
157
  outline
125
- @click="onReset"
126
- title="may be very slow"
127
- icon="border_all"
158
+ icon="search"
128
159
  flat
129
- class="q-mr-sm"
130
- label="Full Graph"
131
- />
132
- </div>
133
- <div class="col-auto">
160
+ class="q-mr-none"
161
+ :disable="!store.state.search.schemaName"
162
+ :loading="store.state.generating"
163
+ @click="onSearch"
164
+ ></q-btn>
134
165
  <q-btn
135
166
  outline
136
- icon="search"
167
+ @click="onReset"
168
+ dense
169
+ title="full graph"
170
+ icon="border_all"
137
171
  flat
138
- label="Search"
139
- class="q-mr-md"
140
- @click="showSearchDialog()"
172
+ class="q-mr-sm"
141
173
  />
142
174
  </div>
143
175
  <div class="col-auto">
@@ -164,7 +196,7 @@
164
196
  double click node to view details.
165
197
  </li>
166
198
  <li>
167
- shift + click to see schema's dependencies without unrelated nodes.
199
+ shift + click to search the schema and highlight related nodes.
168
200
  </li>
169
201
  </ul>
170
202
  </div>
@@ -183,7 +215,6 @@
183
215
  bordered
184
216
  overlay
185
217
  >
186
- <!-- 可拖拽的调整栏 -->
187
218
  <div
188
219
  @mousedown="startDragDrawer"
189
220
  style="
@@ -239,7 +270,7 @@
239
270
  v-for="tag in store.state.leftPanel.tags"
240
271
  :key="tag.name"
241
272
  expand-separator
242
- :model-value="store.state.leftPanel._tag === tag.name"
273
+ :model-value="store.state.leftPanel._tag === tag.name || store.state.search.mode"
243
274
  @update:model-value="(val) => toggleTag(tag.name, val)"
244
275
  :header-class="store.state.leftPanel.tag === tag.name ? 'text-primary text-bold' : ''"
245
276
  content-class="q-pa-none"
@@ -299,7 +330,7 @@
299
330
  <div style="position: relative; width: 100%; height: 100%;">
300
331
  <div id="graph" class="adjust-fit"></div>
301
332
  <div style="position: absolute; left: 8px; top: 8px; z-index: 10; background: rgba(255,255,255,0.85); border-radius: 4px; padding: 2px 8px;">
302
- <div class="q-mt-sm">
333
+ <div class="q-mt-sm" v-if="store.state.modeControl.briefModeEnabled && store.state.search.mode === false">
303
334
  <q-toggle
304
335
  v-if="store.state.modeControl.briefModeEnabled"
305
336
  dense
@@ -309,7 +340,7 @@
309
340
  title="skip middle classes, config module_prefix to enable it"
310
341
  />
311
342
  </div>
312
- <div class="q-mt-sm">
343
+ <div class="q-mt-sm" v-if="store.state.modeControl.briefModeEnabled && store.state.search.mode === false">
313
344
  <q-toggle
314
345
  v-model="store.state.filter.hidePrimitiveRoute"
315
346
  @update:model-value="(val) => toggleHidePrimitiveRoute(val)"
@@ -327,16 +358,6 @@
327
358
  title="show module cluster"
328
359
  />
329
360
  </div>
330
- <div class="q-mt-sm">
331
- <q-toggle
332
- v-model="store.state.modeControl.focus"
333
- v-show="store.state.schemaDetail.schemaCodeName"
334
- @update:model-value="val => onFocusChange(val)"
335
- label="Focus"
336
- dense
337
- title="pick a schema and toggle focus on to display related nodes only"
338
- />
339
- </div>
340
361
  </div>
341
362
  </template>
342
363
  </q-splitter>
@@ -6,6 +6,9 @@ const state = reactive({
6
6
  },
7
7
 
8
8
  version: '',
9
+ config: {
10
+ initial_page_policy: 'first'
11
+ },
9
12
 
10
13
  swagger: {
11
14
  url: ''
@@ -38,13 +41,14 @@ const state = reactive({
38
41
  routeItems: []
39
42
  },
40
43
 
41
- leftPanelFiltered: {
42
-
43
- },
44
44
 
45
45
  // schema options, schema, fields
46
46
  search: {
47
-
47
+ mode: false,
48
+ schemaName: null,
49
+ fieldName: null,
50
+ schemaOptions: [],
51
+ fieldOptions: [],
48
52
  },
49
53
 
50
54
 
@@ -1,16 +1,70 @@
1
- import SchemaFieldFilter from "./component/schema-field-filter.js";
2
1
  import SchemaCodeDisplay from "./component/schema-code-display.js";
3
2
  import RouteCodeDisplay from "./component/route-code-display.js";
4
- import Demo from './component/demo.js'
3
+ import Demo from "./component/demo.js";
5
4
  import RenderGraph from "./component/render-graph.js";
6
5
  import { GraphUI } from "./graph-ui.js";
7
- import { store } from './store.js'
6
+ import { store } from "./store.js";
8
7
 
9
- const { createApp, onMounted, ref } = window.Vue;
8
+ const { createApp, onMounted, ref, watch } = window.Vue;
10
9
 
11
10
  const app = createApp({
12
11
  setup() {
13
12
  let graphUI = null;
13
+ const allSchemaOptions = ref([]);
14
+
15
+ const NBSP = String.fromCharCode(160);
16
+ const formatSchemaLabel = (name, id) =>
17
+ `${name}${NBSP}${NBSP}${NBSP}-${NBSP}${NBSP}${NBSP}${id}`;
18
+
19
+ function rebuildSchemaOptions() {
20
+ const dict = store.state.graph.schemaMap || {};
21
+ const opts = Object.values(dict).map((s) => ({
22
+ label: formatSchemaLabel(s.name, s.id),
23
+ value: s.id,
24
+ }));
25
+ allSchemaOptions.value = opts;
26
+ store.state.search.schemaOptions = opts.slice();
27
+ populateFieldOptions(store.state.search.schemaName);
28
+ }
29
+
30
+ function populateFieldOptions(schemaId) {
31
+ if (!schemaId) {
32
+ store.state.search.fieldOptions = [];
33
+ store.state.search.fieldName = null;
34
+ return;
35
+ }
36
+ const schema = store.state.graph.schemaMap?.[schemaId];
37
+ if (!schema) {
38
+ store.state.search.fieldOptions = [];
39
+ store.state.search.fieldName = null;
40
+ return;
41
+ }
42
+ const fields = Array.isArray(schema.fields)
43
+ ? schema.fields.map((f) => f.name)
44
+ : [];
45
+ store.state.search.fieldOptions = fields;
46
+ if (!fields.includes(store.state.search.fieldName)) {
47
+ store.state.search.fieldName = null;
48
+ }
49
+ }
50
+
51
+ function filterSearchSchemas(val, update) {
52
+ const needle = (val || "").toLowerCase();
53
+ update(() => {
54
+ if (!needle) {
55
+ store.state.search.schemaOptions = allSchemaOptions.value.slice();
56
+ return;
57
+ }
58
+ store.state.search.schemaOptions = allSchemaOptions.value.filter((option) =>
59
+ option.label.toLowerCase().includes(needle)
60
+ );
61
+ });
62
+ }
63
+
64
+ function onSearchSchemaChange(val) {
65
+ store.state.search.schemaName = val;
66
+ store.state.search.mode = false;
67
+ }
14
68
 
15
69
  function readQuerySelection() {
16
70
  if (typeof window === "undefined") {
@@ -55,7 +109,10 @@ const app = createApp({
55
109
 
56
110
  function applySelectionFromQuery(selection) {
57
111
  let applied = false;
58
- if (selection.tag && store.state.leftPanel.tags.some((tag) => tag.name === selection.tag)) {
112
+ if (
113
+ selection.tag &&
114
+ store.state.leftPanel.tags.some((tag) => tag.name === selection.tag)
115
+ ) {
59
116
  store.state.leftPanel.tag = selection.tag;
60
117
  store.state.leftPanel._tag = selection.tag;
61
118
  applied = true;
@@ -72,6 +129,48 @@ const app = createApp({
72
129
  return applied;
73
130
  }
74
131
 
132
+ async function resetSearch() {
133
+ store.state.search.mode = false;
134
+ store.state.leftPanel.tag = null;
135
+ store.state.leftPanel._tag = null;
136
+ store.state.leftPanel.routeId = null;
137
+ await loadSearchedTags();
138
+ renderBasedOnInitialPolicy()
139
+ }
140
+
141
+ async function onSearch() {
142
+ store.state.search.mode = true;
143
+ store.state.leftPanel.tag = null;
144
+ store.state.leftPanel._tag = null;
145
+ store.state.leftPanel.routeId = null;
146
+ await loadSearchedTags();
147
+ await onGenerate();
148
+ }
149
+ async function loadSearchedTags() {
150
+ try {
151
+ const payload = {
152
+ schema_name: store.state.search.schemaName,
153
+ schema_field: store.state.search.fieldName || null,
154
+ show_fields: store.state.filter.showFields,
155
+ brief: store.state.filter.brief,
156
+ hide_primitive_route: store.state.filter.hidePrimitiveRoute,
157
+ show_module: store.state.filter.showModule,
158
+ };
159
+ const res = await fetch("dot-search", {
160
+ method: "POST",
161
+ headers: { "Content-Type": "application/json" },
162
+ body: JSON.stringify(payload),
163
+ });
164
+ if (res.ok) {
165
+ const data = await res.json();
166
+ const tags = Array.isArray(data.tags) ? data.tags : [];
167
+ store.state.leftPanel.tags = tags;
168
+ }
169
+ } catch (err) {
170
+ console.error("dot-search failed", err);
171
+ }
172
+ }
173
+
75
174
  async function loadInitial() {
76
175
  store.state.initializing = true;
77
176
  try {
@@ -81,10 +180,8 @@ const app = createApp({
81
180
 
82
181
  const schemasArr = Array.isArray(data.schemas) ? data.schemas : [];
83
182
  // Build dict keyed by id for faster lookups and simpler prop passing
84
- const schemaMap = Object.fromEntries(
85
- schemasArr.map((s) => [s.id, s])
86
- );
87
- store.state.graph.schemaMap = schemaMap
183
+ const schemaMap = Object.fromEntries(schemasArr.map((s) => [s.id, s]));
184
+ store.state.graph.schemaMap = schemaMap;
88
185
  store.state.graph.schemaKeys = new Set(Object.keys(schemaMap));
89
186
  store.state.graph.routeItems = data.tags
90
187
  .map((t) => t.routes)
@@ -93,9 +190,12 @@ const app = createApp({
93
190
  acc[r.id] = r;
94
191
  return acc;
95
192
  }, {});
96
- store.state.modeControl.briefModeEnabled = data.enable_brief_mode || false;
193
+ store.state.modeControl.briefModeEnabled =
194
+ data.enable_brief_mode || false;
97
195
  store.state.version = data.version || "";
98
- store.state.swagger.url = data.swagger_url || null
196
+ store.state.swagger.url = data.swagger_url || null;
197
+
198
+ rebuildSchemaOptions();
99
199
 
100
200
  const querySelection = readQuerySelection();
101
201
  const restoredFromQuery = applySelectionFromQuery(querySelection);
@@ -104,19 +204,8 @@ const app = createApp({
104
204
  onGenerate();
105
205
  return;
106
206
  }
107
-
108
- switch (data.initial_page_policy) {
109
- case "full":
110
- onGenerate()
111
- return
112
- case "empty":
113
- return
114
- case "first":
115
- store.state.leftPanel.tag = store.state.leftPanel.tags.length > 0 ? store.state.leftPanel.tags[0].name : null;
116
- store.state.leftPanel._tag = store.state.leftPanel.tag;
117
- onGenerate();
118
- return
119
- }
207
+ store.state.config.initial_page_policy = data.initial_page_policy
208
+ renderBasedOnInitialPolicy()
120
209
 
121
210
  // default route options placeholder
122
211
  } catch (e) {
@@ -126,28 +215,40 @@ const app = createApp({
126
215
  }
127
216
  }
128
217
 
129
- async function onFocusChange(val) {
130
- if (val) {
131
- await onGenerate(true); // target could be out of view when switchingfrom big to small
132
- } else {
133
- await onGenerate(false);
134
- setTimeout(() => {
135
- const ele = $(`[data-name='${store.state.schemaDetail.schemaCodeName}'] polygon`);
136
- ele.dblclick();
137
- }, 1);
138
- }
218
+ async function renderBasedOnInitialPolicy() {
219
+ switch (store.state.config.initial_page_policy) {
220
+ case "full":
221
+ onGenerate();
222
+ return;
223
+ case "empty":
224
+ return;
225
+ case "first":
226
+ store.state.leftPanel.tag =
227
+ store.state.leftPanel.tags.length > 0
228
+ ? store.state.leftPanel.tags[0].name
229
+ : null;
230
+ store.state.leftPanel._tag = store.state.leftPanel.tag;
231
+ onGenerate();
232
+ return;
233
+ }
139
234
  }
140
235
 
141
236
  async function onGenerate(resetZoom = true) {
142
- const schema_name = store.state.modeControl.focus ? store.state.schemaDetail.schemaCodeName : null;
237
+ const activeSchema = store.state.search.mode
238
+ ? store.state.search.schemaName
239
+ : null;
240
+ const activeField = store.state.search.mode
241
+ ? store.state.search.fieldName
242
+ : null;
143
243
  store.state.generating = true;
144
244
  try {
145
245
  const payload = {
146
246
  tags: store.state.leftPanel.tag ? [store.state.leftPanel.tag] : null,
147
- schema_name: schema_name || null,
247
+ schema_name: activeSchema || null,
248
+ schema_field: activeField || null,
148
249
  route_name: store.state.leftPanel.routeId || null,
149
250
  show_fields: store.state.filter.showFields,
150
- brief: store.state.modeControl.brief,
251
+ brief: store.state.filter.brief,
151
252
  hide_primitive_route: store.state.filter.hidePrimitiveRoute,
152
253
  show_module: store.state.filter.showModule,
153
254
  };
@@ -163,9 +264,9 @@ const app = createApp({
163
264
  graphUI = new GraphUI("#graph", {
164
265
  onSchemaShiftClick: (id) => {
165
266
  if (store.state.graph.schemaKeys.has(id)) {
166
- resetDetailPanels();
167
- store.state.searchDialog.show = true;
168
- store.state.searchDialog.schema = id;
267
+ store.state.search.mode = true;
268
+ store.state.search.schemaName = id;
269
+ onSearch();
169
270
  }
170
271
  },
171
272
  onSchemaClick: (id) => {
@@ -192,14 +293,9 @@ const app = createApp({
192
293
  }
193
294
  }
194
295
 
195
- function showSearchDialog() {
196
- store.state.searchDialog.show = true;
197
- store.state.searchDialog.schema = null;
198
- }
199
-
200
296
  function resetDetailPanels() {
201
297
  store.state.rightDrawer.drawer = false;
202
- store.state.routeDetail.show = false
298
+ store.state.routeDetail.show = false;
203
299
  store.state.schemaDetail.schemaCodeName = "";
204
300
  }
205
301
 
@@ -210,11 +306,14 @@ const app = createApp({
210
306
 
211
307
  store.state.graph.schemaId = null;
212
308
 
213
- // state.showFields = "object";
214
- store.state.modeControl.focus = false;
309
+ store.state.search.mode = false;
310
+ store.state.search.schemaName = null;
311
+ store.state.search.fieldName = null;
312
+ store.state.search.fieldOptions = [];
313
+ store.state.search.schemaOptions = allSchemaOptions.value.slice();
314
+
215
315
  store.state.schemaDetail.schemaCodeName = "";
216
- onGenerate();
217
- syncSelectionToUrl();
316
+ resetSearch()
218
317
  }
219
318
 
220
319
  function toggleTag(tagName, expanded = null) {
@@ -223,7 +322,6 @@ const app = createApp({
223
322
  store.state.leftPanel.tag = tagName;
224
323
  store.state.leftPanel.routeId = "";
225
324
 
226
- store.state.modeControl.focus = false;
227
325
  store.state.schemaDetail.schemaCodeName = "";
228
326
  onGenerate();
229
327
  } else {
@@ -231,7 +329,7 @@ const app = createApp({
231
329
  }
232
330
 
233
331
  store.state.rightDrawer.drawer = false;
234
- store.state.routeDetail.show = false
332
+ store.state.routeDetail.show = false;
235
333
  syncSelectionToUrl();
236
334
  }
237
335
 
@@ -242,8 +340,7 @@ const app = createApp({
242
340
  store.state.leftPanel.routeId = routeId;
243
341
  }
244
342
  store.state.rightDrawer.drawer = false;
245
- store.state.routeDetail.show = false
246
- store.state.modeControl.focus = false;
343
+ store.state.routeDetail.show = false;
247
344
  store.state.schemaDetail.schemaCodeName = "";
248
345
  onGenerate();
249
346
  syncSelectionToUrl();
@@ -251,7 +348,7 @@ const app = createApp({
251
348
 
252
349
  function toggleShowModule(val) {
253
350
  store.state.filter.showModule = val;
254
- onGenerate()
351
+ onGenerate();
255
352
  }
256
353
 
257
354
  function toggleShowField(field) {
@@ -260,7 +357,7 @@ const app = createApp({
260
357
  }
261
358
 
262
359
  function toggleBrief(val) {
263
- store.state.modeControl.brief = val;
360
+ store.state.filter.brief = val;
264
361
  onGenerate();
265
362
  }
266
363
 
@@ -293,24 +390,45 @@ const app = createApp({
293
390
  e.preventDefault();
294
391
  }
295
392
 
393
+ watch(
394
+ () => store.state.graph.schemaMap,
395
+ () => {
396
+ rebuildSchemaOptions();
397
+ },
398
+ { deep: false }
399
+ );
400
+
401
+ watch(
402
+ () => store.state.search.schemaName,
403
+ (schemaId) => {
404
+ store.state.search.schemaOptions = allSchemaOptions.value.slice();
405
+ populateFieldOptions(schemaId);
406
+ if (!schemaId) {
407
+ store.state.search.mode = false;
408
+ }
409
+ }
410
+ );
411
+
296
412
  onMounted(async () => {
297
- document.body.classList.remove("app-loading")
413
+ document.body.classList.remove("app-loading");
298
414
  await loadInitial();
299
415
  // Reveal app content only after initial JS/data is ready
300
416
  });
301
417
 
302
418
  return {
303
419
  store,
420
+ onSearch,
421
+ resetSearch,
422
+ filterSearchSchemas,
423
+ onSearchSchemaChange,
304
424
  toggleTag,
305
425
  toggleBrief,
306
426
  toggleHidePrimitiveRoute,
307
427
  selectRoute,
308
428
  onGenerate,
309
429
  onReset,
310
- showSearchDialog,
311
430
  toggleShowField,
312
431
  startDragDrawer,
313
- onFocusChange,
314
432
  toggleShowModule,
315
433
  };
316
434
  },
@@ -323,10 +441,9 @@ if (window.Quasar && typeof window.Quasar.setCssVar === "function") {
323
441
  window.Quasar.setCssVar("primary", "#009485");
324
442
  }
325
443
 
326
- app.component("schema-field-filter", SchemaFieldFilter); // shift click and see relationships
327
- app.component("schema-code-display", SchemaCodeDisplay); // double click to see node details
328
- app.component("route-code-display", RouteCodeDisplay); // double click to see route details
329
- app.component("render-graph", RenderGraph); // for debug, render pasted dot content
330
- app.component('demo-component', Demo)
444
+ app.component("schema-code-display", SchemaCodeDisplay); // double click to see node details
445
+ app.component("route-code-display", RouteCodeDisplay); // double click to see route details
446
+ app.component("render-graph", RenderGraph); // for debug, render pasted dot content
447
+ app.component("demo-component", Demo);
331
448
 
332
449
  app.mount("#q-app");
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.12.5
3
+ Version: 0.12.7
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
@@ -3,24 +3,23 @@ fastapi_voyager/cli.py,sha256=td3yIIigEomhSdDO-Xkh-CgpEwCafwlwnpvxnT9QsBo,10488
3
3
  fastapi_voyager/filter.py,sha256=AN_HIu8-DtKisIq5mFt7CnqRHtxKewedNGyyaI82hSY,11529
4
4
  fastapi_voyager/module.py,sha256=h9YR3BpS-CAcJW9WCdVkF4opqwY32w9T67g9GfdLytk,3425
5
5
  fastapi_voyager/render.py,sha256=7jSChISqTV2agaO55thhwyrOhqVOMux4x7k8rSQROnU,9960
6
- fastapi_voyager/server.py,sha256=MZNRpcXor2q8Rj3OSf6EH8NkgDChxfzUtnIY8ilRkaY,7053
6
+ fastapi_voyager/server.py,sha256=wwCFl1eeq_jn8nfIwcy7s8Sh6MB55SN8u9AsJRnkFWQ,7979
7
7
  fastapi_voyager/type.py,sha256=7EL1zaIwKVRGpLig7fqaOrZGN5k0Rm31C9COfck3CSs,1750
8
8
  fastapi_voyager/type_helper.py,sha256=JXD_OE_xTARkGjWDsnO_xfvyZ0vcwViYyqCp6oEHBTM,9719
9
- fastapi_voyager/version.py,sha256=0gsRRMzt46OBC-7IuF0VTu6UPwKs_X7qmw7KMjDhIeo,49
10
- fastapi_voyager/voyager.py,sha256=LiRUb0ZG2cfnyY_pwRqoeZjxb6Pu6xy_lqPiMupxoKM,13510
11
- fastapi_voyager/web/graph-ui.js,sha256=NiwUFHCZPE4C-Hx4qwFvHwPyXw_lu2ar3WnPkmVGQN0,5850
9
+ fastapi_voyager/version.py,sha256=z8XZy0W66ufD2jmi9zvVlH2vjRJoy4CC-ZR7EhfBej8,49
10
+ fastapi_voyager/voyager.py,sha256=qJZvkLh5W_18X5B4pJ8XYbytx2Wvcz-0OnYoHWw8XlA,14046
11
+ fastapi_voyager/web/graph-ui.js,sha256=wxKjmVEpaMJZXMTW7tJ4BiaACCI1oVN6cx7hSMI0K5U,6428
12
12
  fastapi_voyager/web/graphviz.svg.css,sha256=zDCjjpT0Idufu5YOiZI76PL70-avP3vTyzGPh9M85Do,1563
13
- fastapi_voyager/web/graphviz.svg.js,sha256=lvAdbjHc-lMSk4GQp-iqYA2PCFX4RKnW7dFaoe0LUHs,16005
14
- fastapi_voyager/web/index.html,sha256=3HixCgVF9VtTx89G3usDXpEh7NZnLk_p9siz4OUVUHE,17444
13
+ fastapi_voyager/web/graphviz.svg.js,sha256=wZwz_lBztoXmujEN21P0w-HMpdmbqPwTQQ6Ebxd9rGo,18569
14
+ fastapi_voyager/web/index.html,sha256=L2xt6T-b4V6WnYnZ_MkmduDQLzFPxfRLhHuiaG9QnGU,18362
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/store.js,sha256=fW-3uUwWNUsW8vXbTqltHvvlIroBeZTJvFkMSjuWdVg,1630
18
- fastapi_voyager/web/vue-main.js,sha256=H50LbB-g3jWaz3Xv2O8RgajlibjtoiHu1HDTumjf7sY,10766
17
+ fastapi_voyager/web/store.js,sha256=ul6I00XqT9FByAD20JgO2mzdbYCZhTI-CP5t0g_k6yo,1776
18
+ fastapi_voyager/web/vue-main.js,sha256=LoMVDcU4KSTw3mcquI3g3xJT8ChcQtiyC1rGV79tpUA,14208
19
19
  fastapi_voyager/web/component/demo.js,sha256=bQb16Un4XZ3Mf8qL6gvyrXe_mmA3V3mSIRMQAWg2MNk,352
20
20
  fastapi_voyager/web/component/render-graph.js,sha256=e8Xgh2Kl-nYU0P1gstEmAepCgFnk2J6UdxW8TlMafGs,2322
21
21
  fastapi_voyager/web/component/route-code-display.js,sha256=8NJPPjNRUC21gjpY8XYEQs4RBbhX1pCiqEhJp39ku6k,3678
22
22
  fastapi_voyager/web/component/schema-code-display.js,sha256=qKUMV2RFQzR8deof2iC4vyp65UaWadtVsDAXjY-i3vE,7042
23
- fastapi_voyager/web/component/schema-field-filter.js,sha256=i6Zp02wfSBWQb4AJ7H_Q9vgCVzLaVnH1I2JIot0sV-0,6172
24
23
  fastapi_voyager/web/icon/android-chrome-192x192.png,sha256=35sBy6jmUFJCcquStaafHH1qClZIbd-X3PIKSeLkrNo,37285
25
24
  fastapi_voyager/web/icon/android-chrome-512x512.png,sha256=eb2eDjCwIruc05029_0L9hcrkVkv8KceLn1DJMYU0zY,210789
26
25
  fastapi_voyager/web/icon/apple-touch-icon.png,sha256=gnWK46tPnvSw1-oYZjgI02wpoO4OrIzsVzGHC5oKWO0,33187
@@ -28,8 +27,8 @@ fastapi_voyager/web/icon/favicon-16x16.png,sha256=JC07jEzfIYxBIoQn_FHXvyHuxESdhW
28
27
  fastapi_voyager/web/icon/favicon-32x32.png,sha256=C7v1h58cfWOsiLp9yOIZtlx-dLasBcq3NqpHVGRmpt4,1859
29
28
  fastapi_voyager/web/icon/favicon.ico,sha256=tZolYIXkkBcFiYl1A8ksaXN2VjGamzcSdes838dLvNc,15406
30
29
  fastapi_voyager/web/icon/site.webmanifest,sha256=ep4Hzh9zhmiZF2At3Fp1dQrYQuYF_3ZPZxc1KcGBvwQ,263
31
- fastapi_voyager-0.12.5.dist-info/METADATA,sha256=QO3zauma0JaAR_DyhX5yQqNkDf868_2cAw1mHHKY3o0,6523
32
- fastapi_voyager-0.12.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
33
- fastapi_voyager-0.12.5.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
34
- fastapi_voyager-0.12.5.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
35
- fastapi_voyager-0.12.5.dist-info/RECORD,,
30
+ fastapi_voyager-0.12.7.dist-info/METADATA,sha256=ZsBh0iYCI0KSjBlTW_WQuwBglfwNHpSIrjZee3u4eUg,6523
31
+ fastapi_voyager-0.12.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
32
+ fastapi_voyager-0.12.7.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
33
+ fastapi_voyager-0.12.7.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
34
+ fastapi_voyager-0.12.7.dist-info/RECORD,,
@@ -1,190 +0,0 @@
1
- import { GraphUI } from "../graph-ui.js";
2
- const { defineComponent, reactive, ref, onMounted, nextTick, watch } =
3
- window.Vue;
4
-
5
- // SchemaFieldFilter component
6
- // Features:
7
- // - Fetch initial schemas list (GET /dot) and build schema options
8
- // - Second selector lists fields of the chosen schema
9
- // - Query button disabled until a schema is selected
10
- // - On query: POST /dot with schema_name + optional schema_field; render returned DOT in #graph-schema-field
11
- // - Uses GraphUI once and re-renders
12
- // - Emits 'queried' event after successful render (payload: { schemaName, fieldName })
13
- export default defineComponent({
14
- name: "SchemaFieldFilter",
15
- props: {
16
- schemaName: { type: String, default: null }, // external injection triggers auto-query
17
- schemas: { type: Object, default: () => ({}) },
18
- },
19
- emits: ["queried", "close"],
20
- setup(props, { emit }) {
21
- const state = reactive({
22
- loadingSchemas: false,
23
- querying: false,
24
- schemas: [], // [{ name, fullname, fields: [{name,...}] }]
25
- schemaOptions: [], // [{ label, value }]
26
- fieldOptions: [], // [ field.name ]
27
- schemaFullname: null,
28
- fieldName: null,
29
- error: null,
30
- showFields: "object",
31
- showFieldOptions: [
32
- { label: "No fields", value: "single" },
33
- { label: "Object fields", value: "object" },
34
- { label: "All fields", value: "all" },
35
- ],
36
- });
37
-
38
- let graphInstance = null;
39
- let lastAppliedExternal = null;
40
-
41
- async function loadSchemas() {
42
- // Use externally provided props.schemas dict directly; no network call.
43
- state.error = null;
44
- const dict =
45
- props.schemas && typeof props.schemas === "object" ? props.schemas : {};
46
- // Flatten to array for local operations
47
- state.schemas = Object.values(dict);
48
- state.schemaOptions = state.schemas.map((s) => ({
49
- label: `${s.name} - ${s.id}`,
50
- value: s.id,
51
- }));
52
- // Maintain compatibility: loadingSchemas flag toggled quickly (no async work)
53
- state.loadingSchemas = false;
54
- }
55
-
56
- function onFilterSchemas(val, update) {
57
- const needle = (val || "").toLowerCase();
58
- update(() => {
59
- let opts = state.schemas.map((s) => ({
60
- label: `${s.name} - ${s.id}`,
61
- value: s.id,
62
- }));
63
- if (needle) {
64
- opts = opts.filter((o) => o.label.toLowerCase().includes(needle));
65
- }
66
- state.schemaOptions = opts;
67
- });
68
- }
69
-
70
- function onSchemaChange(val) {
71
- state.schemaFullname = val;
72
- state.fieldName = null;
73
- const schema = state.schemas.find((s) => s.id === val);
74
- state.fieldOptions = schema ? schema.fields.map((f) => f.name) : [];
75
- }
76
-
77
- async function onQuery() {
78
- if (!state.schemaFullname) return;
79
- state.querying = true;
80
- state.error = null;
81
- try {
82
- const payload = {
83
- schema_name: state.schemaFullname,
84
- schema_field: state.fieldName || null,
85
- show_fields: state.showFields,
86
- };
87
- const res = await fetch("dot", {
88
- method: "POST",
89
- headers: { "Content-Type": "application/json" },
90
- body: JSON.stringify(payload),
91
- });
92
- const dotText = await res.text();
93
- if (!graphInstance) {
94
- graphInstance = new GraphUI("#graph-schema-field");
95
- }
96
- await graphInstance.render(dotText);
97
- emit("queried", {
98
- schemaName: state.schemaFullname,
99
- fieldName: state.fieldName,
100
- });
101
- } catch (e) {
102
- state.error = "Query failed";
103
- console.error("SchemaFieldFilter query failed", e);
104
- } finally {
105
- state.querying = false;
106
- }
107
- }
108
-
109
- function applyExternalSchema(name) {
110
- if (!name || !state.schemas.length) return;
111
- if (lastAppliedExternal === name) return; // avoid duplicate
112
- const schema = state.schemas.find((s) => s.id === name);
113
- if (!schema) return;
114
- state.schemaFullname = schema.id;
115
- state.fieldOptions = schema.fields.map((f) => f.name);
116
- state.fieldName = null; // reset field for external injection
117
- lastAppliedExternal = name;
118
- // auto query
119
- onQuery();
120
- }
121
-
122
- onMounted(async () => {
123
- await nextTick();
124
- await loadSchemas();
125
- if (props.schemaName) {
126
- applyExternalSchema(props.schemaName);
127
- }
128
- });
129
-
130
- function close() {
131
- emit("close");
132
- }
133
-
134
- return { state, onSchemaChange, onQuery, close, onFilterSchemas };
135
- },
136
- template: `
137
- <div style="height:100%; position:relative; background:#fff;">
138
- <div style="position:absolute; top:8px; left:8px; z-index:10; background:rgba(255,255,255,0.95); padding:8px 10px; border-radius:4px; box-shadow:0 1px 3px rgba(0,0,0,0.15);" class="q-gutter-sm row items-center">
139
- <q-select
140
- dense outlined use-input input-debounce="0"
141
- v-model="state.schemaFullname"
142
- :options="state.schemaOptions"
143
- option-label="label"
144
- option-value="value"
145
- emit-value
146
- map-options
147
- :loading="state.loadingSchemas"
148
- style="min-width:220px"
149
- clearable
150
- label="Select schema"
151
- @update:model-value="onSchemaChange"
152
- @filter="onFilterSchemas"
153
- />
154
- <q-select
155
- dense outlined
156
- v-model="state.fieldName"
157
- :disable="!state.schemaFullname || state.fieldOptions.length===0"
158
- :options="state.fieldOptions"
159
- style="min-width:180px"
160
- clearable
161
- label="Select field (optional)"
162
- />
163
- <q-option-group
164
- v-model="state.showFields"
165
- :options="state.showFieldOptions"
166
- type="radio"
167
- inline
168
- dense
169
- color="primary"
170
- style="min-width:260px"
171
- />
172
- <q-btn
173
- class="q-ml-md"
174
- icon="search"
175
- label="Search"
176
- outline
177
- :disable="!state.schemaFullname"
178
- :loading="state.querying"
179
- @click="onQuery" />
180
- <q-btn
181
- flat dense round icon="close"
182
- aria-label="Close"
183
- @click="close"
184
- />
185
- </div>
186
- <div v-if="state.error" style="position:absolute; top:52px; left:8px; z-index:10; color:#c10015; font-size:12px;">{{ state.error }}</div>
187
- <div id="graph-schema-field" style="width:100%; height:100%; overflow:auto; background:#fafafa"></div>
188
- </div>
189
- `,
190
- });