fastapi-voyager 0.12.6__tar.gz → 0.12.8__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 (59) hide show
  1. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/PKG-INFO +1 -1
  2. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/docs/changelog.md +5 -1
  3. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/server.py +32 -1
  4. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/version.py +1 -1
  5. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/voyager.py +16 -0
  6. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/index.html +58 -36
  7. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/store.js +8 -4
  8. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/vue-main.js +185 -71
  9. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/tests/programatic.py +1 -1
  10. fastapi_voyager-0.12.6/src/fastapi_voyager/web/component/schema-field-filter.js +0 -190
  11. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/.github/workflows/publish.yml +0 -0
  14. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/.gitignore +0 -0
  15. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/.python-version +0 -0
  16. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/CONTRIBUTING.md +0 -0
  17. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/LICENSE +0 -0
  18. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/README.md +0 -0
  19. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/docs/idea.md +0 -0
  20. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/pyproject.toml +0 -0
  21. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/release.md +0 -0
  22. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/__init__.py +0 -0
  23. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/cli.py +0 -0
  24. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/filter.py +0 -0
  25. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/module.py +0 -0
  26. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/render.py +0 -0
  27. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/type.py +0 -0
  28. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/type_helper.py +0 -0
  29. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/component/demo.js +0 -0
  30. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/component/render-graph.js +0 -0
  31. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
  32. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
  33. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/graph-ui.js +0 -0
  34. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
  35. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
  36. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
  37. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
  38. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
  39. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
  40. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
  41. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
  42. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
  43. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/quasar.min.css +0 -0
  44. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/src/fastapi_voyager/web/quasar.min.js +0 -0
  45. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/tests/__init__.py +0 -0
  46. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/tests/demo.py +0 -0
  47. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/tests/demo_anno.py +0 -0
  48. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/tests/service/__init__.py +0 -0
  49. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/tests/service/schema/__init__.py +0 -0
  50. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/tests/service/schema/extra.py +0 -0
  51. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/tests/service/schema/schema.py +0 -0
  52. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/tests/test_analysis.py +0 -0
  53. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/tests/test_filter.py +0 -0
  54. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/tests/test_generic.py +0 -0
  55. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/tests/test_import.py +0 -0
  56. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/tests/test_module.py +0 -0
  57. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/tests/test_type_helper.py +0 -0
  58. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/uv.lock +0 -0
  59. {fastapi_voyager-0.12.6 → fastapi_voyager-0.12.8}/voyager.jpg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.12.6
3
+ Version: 0.12.8
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
@@ -120,8 +120,12 @@
120
120
  - [x] click link(edge) to highlight related nodes
121
121
  - [x] on hover cursor effect
122
122
  - 0.12.7
123
+ - [x] remove search component, integrated into main page
124
+ - 0.12.8
125
+ - [x] optimize ui elements, change icons, update reset behavior
126
+ - 0.12.9
127
+ - [ ] refactor vue-main.js, move methods to store
123
128
  - [ ] refactor render.py
124
- - [ ] remove search component, integrated into main page
125
129
 
126
130
  ## 0.13
127
131
  - 0.13.2
@@ -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.6"
2
+ __version__ = "0.12.8"
@@ -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,
@@ -55,6 +55,12 @@
55
55
  flex: 1 1 auto;
56
56
  overflow: auto;
57
57
  }
58
+ .tag-bg {
59
+ /* background-color: #287e75; */
60
+ /* border-bottom: 2px solid #009485;
61
+ border-left: 2px solid #009485;
62
+ border-top: 0; */
63
+ }
58
64
 
59
65
  .adjust-fit {
60
66
  height: calc(100vh - 54px);
@@ -107,9 +113,20 @@
107
113
  <span> FastAPI Voyager </span>
108
114
  <span v-if="store.state.version" style="font-size: 12px; margin-left: 8px; font-weight: normal;">{{ store.state.version }}</span>
109
115
  </div>
116
+ <div class="col-auto">
117
+ <q-btn
118
+ style="margin-left: 80px"
119
+ outline
120
+ @click="onReset"
121
+ title="clear tag, route selection"
122
+ icon="open_in_full"
123
+ color="primary"
124
+ flat
125
+ class="q-mx-md"
126
+ />
127
+ </div>
110
128
  <div class="col-auto" style="font-size: 16px">
111
129
  <q-option-group
112
- style="margin-left: 80px"
113
130
  v-model="store.state.filter.showFields"
114
131
  :options="store.state.fieldOptions"
115
132
  @update:model-value="(val) => toggleShowField(val)"
@@ -120,25 +137,40 @@
120
137
  />
121
138
  </div>
122
139
  <div class="col-auto q-ml-auto">
123
- <q-btn
124
- outline
125
- @click="onReset"
126
- title="may be very slow"
127
- icon="border_all"
128
- flat
129
- class="q-mr-sm"
130
- label="Full Graph"
131
- />
132
140
  </div>
133
- <div class="col-auto">
134
- <q-btn
135
- outline
136
- icon="search"
137
- flat
138
- label="Search"
139
- class="q-mr-md"
140
- @click="showSearchDialog()"
141
- />
141
+ <div class="col-auto row items-center q-gutter-sm">
142
+ <q-select
143
+ dense
144
+ outlined
145
+ use-input
146
+ fill-input
147
+ hide-selected
148
+ input-debounce="200"
149
+ v-model="store.state.search.schemaName"
150
+ :options="store.state.search.schemaOptions"
151
+ option-label="label"
152
+ option-value="value"
153
+ emit-value
154
+ map-options
155
+ style="min-width: 320px;"
156
+ clearable
157
+ label="Select schema"
158
+ @update:model-value="onSearchSchemaChange"
159
+ @filter="filterSearchSchemas"
160
+ @clear="resetSearch"
161
+ ></q-select>
162
+
163
+ <q-select
164
+ dense
165
+ outlined
166
+ v-model="store.state.search.fieldName"
167
+ :disable="!store.state.search.schemaName || store.state.search.fieldOptions.length===0"
168
+ :options="store.state.search.fieldOptions"
169
+ @update:model-value="onSearch"
170
+ style="min-width: 180px"
171
+ clearable
172
+ label="Select field (optional)"
173
+ ></q-select>
142
174
  </div>
143
175
  <div class="col-auto">
144
176
  <q-btn
@@ -147,7 +179,7 @@
147
179
  flat
148
180
  icon="help_outline"
149
181
  aria-label="Help"
150
- style="margin-right: 40px"
182
+ style="margin-right: 50px; margin-left: 20px"
151
183
  >
152
184
  <q-tooltip
153
185
  anchor="bottom middle"
@@ -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,9 +270,10 @@
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
- :header-class="store.state.leftPanel.tag === tag.name ? 'text-primary text-bold' : ''"
275
+ @click="() => toggleTag(tag.name, true)"
276
+ :header-class="store.state.leftPanel.tag === tag.name ? 'text-primary text-bold tag-bg' : 'tag-bg'"
245
277
  content-class="q-pa-none"
246
278
  >
247
279
  <template #header>
@@ -299,7 +331,7 @@
299
331
  <div style="position: relative; width: 100%; height: 100%;">
300
332
  <div id="graph" class="adjust-fit"></div>
301
333
  <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">
334
+ <div class="q-mt-sm" v-if="store.state.modeControl.briefModeEnabled && store.state.search.mode === false">
303
335
  <q-toggle
304
336
  v-if="store.state.modeControl.briefModeEnabled"
305
337
  dense
@@ -309,7 +341,7 @@
309
341
  title="skip middle classes, config module_prefix to enable it"
310
342
  />
311
343
  </div>
312
- <div class="q-mt-sm">
344
+ <div class="q-mt-sm" v-if="store.state.modeControl.briefModeEnabled && store.state.search.mode === false">
313
345
  <q-toggle
314
346
  v-model="store.state.filter.hidePrimitiveRoute"
315
347
  @update:model-value="(val) => toggleHidePrimitiveRoute(val)"
@@ -327,16 +359,6 @@
327
359
  title="show module cluster"
328
360
  />
329
361
  </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
362
  </div>
341
363
  </template>
342
364
  </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,71 @@
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
+ onSearch()
68
+ }
14
69
 
15
70
  function readQuerySelection() {
16
71
  if (typeof window === "undefined") {
@@ -55,7 +110,10 @@ const app = createApp({
55
110
 
56
111
  function applySelectionFromQuery(selection) {
57
112
  let applied = false;
58
- if (selection.tag && store.state.leftPanel.tags.some((tag) => tag.name === selection.tag)) {
113
+ if (
114
+ selection.tag &&
115
+ store.state.leftPanel.tags.some((tag) => tag.name === selection.tag)
116
+ ) {
59
117
  store.state.leftPanel.tag = selection.tag;
60
118
  store.state.leftPanel._tag = selection.tag;
61
119
  applied = true;
@@ -72,6 +130,51 @@ const app = createApp({
72
130
  return applied;
73
131
  }
74
132
 
133
+ async function resetSearch() {
134
+ store.state.search.mode = false;
135
+ store.state.leftPanel.tag = null;
136
+ store.state.leftPanel._tag = null;
137
+ store.state.leftPanel.routeId = null;
138
+ syncSelectionToUrl()
139
+ await loadSearchedTags();
140
+ renderBasedOnInitialPolicy()
141
+ }
142
+
143
+ async function onSearch() {
144
+ console.log('start search')
145
+ store.state.search.mode = true;
146
+ store.state.leftPanel.tag = null;
147
+ store.state.leftPanel._tag = null;
148
+ store.state.leftPanel.routeId = null;
149
+ syncSelectionToUrl()
150
+ await loadSearchedTags();
151
+ await onGenerate();
152
+ }
153
+ async function loadSearchedTags() {
154
+ try {
155
+ const payload = {
156
+ schema_name: store.state.search.schemaName,
157
+ schema_field: store.state.search.fieldName || null,
158
+ show_fields: store.state.filter.showFields,
159
+ brief: store.state.filter.brief,
160
+ hide_primitive_route: store.state.filter.hidePrimitiveRoute,
161
+ show_module: store.state.filter.showModule,
162
+ };
163
+ const res = await fetch("dot-search", {
164
+ method: "POST",
165
+ headers: { "Content-Type": "application/json" },
166
+ body: JSON.stringify(payload),
167
+ });
168
+ if (res.ok) {
169
+ const data = await res.json();
170
+ const tags = Array.isArray(data.tags) ? data.tags : [];
171
+ store.state.leftPanel.tags = tags;
172
+ }
173
+ } catch (err) {
174
+ console.error("dot-search failed", err);
175
+ }
176
+ }
177
+
75
178
  async function loadInitial() {
76
179
  store.state.initializing = true;
77
180
  try {
@@ -81,10 +184,8 @@ const app = createApp({
81
184
 
82
185
  const schemasArr = Array.isArray(data.schemas) ? data.schemas : [];
83
186
  // 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
187
+ const schemaMap = Object.fromEntries(schemasArr.map((s) => [s.id, s]));
188
+ store.state.graph.schemaMap = schemaMap;
88
189
  store.state.graph.schemaKeys = new Set(Object.keys(schemaMap));
89
190
  store.state.graph.routeItems = data.tags
90
191
  .map((t) => t.routes)
@@ -93,9 +194,12 @@ const app = createApp({
93
194
  acc[r.id] = r;
94
195
  return acc;
95
196
  }, {});
96
- store.state.modeControl.briefModeEnabled = data.enable_brief_mode || false;
197
+ store.state.modeControl.briefModeEnabled =
198
+ data.enable_brief_mode || false;
97
199
  store.state.version = data.version || "";
98
- store.state.swagger.url = data.swagger_url || null
200
+ store.state.swagger.url = data.swagger_url || null;
201
+
202
+ rebuildSchemaOptions();
99
203
 
100
204
  const querySelection = readQuerySelection();
101
205
  const restoredFromQuery = applySelectionFromQuery(querySelection);
@@ -103,19 +207,9 @@ const app = createApp({
103
207
  syncSelectionToUrl();
104
208
  onGenerate();
105
209
  return;
106
- }
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
210
+ } else {
211
+ store.state.config.initial_page_policy = data.initial_page_policy
212
+ renderBasedOnInitialPolicy()
119
213
  }
120
214
 
121
215
  // default route options placeholder
@@ -126,28 +220,41 @@ const app = createApp({
126
220
  }
127
221
  }
128
222
 
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
- }
223
+ async function renderBasedOnInitialPolicy() {
224
+ switch (store.state.config.initial_page_policy) {
225
+ case "full":
226
+ onGenerate();
227
+ return;
228
+ case "empty":
229
+ return;
230
+ case "first":
231
+ store.state.leftPanel.tag =
232
+ store.state.leftPanel.tags.length > 0
233
+ ? store.state.leftPanel.tags[0].name
234
+ : null;
235
+ store.state.leftPanel._tag = store.state.leftPanel.tag;
236
+ syncSelectionToUrl()
237
+ onGenerate();
238
+ return;
239
+ }
139
240
  }
140
241
 
141
242
  async function onGenerate(resetZoom = true) {
142
- const schema_name = store.state.modeControl.focus ? store.state.schemaDetail.schemaCodeName : null;
243
+ const activeSchema = store.state.search.mode
244
+ ? store.state.search.schemaName
245
+ : null;
246
+ const activeField = store.state.search.mode
247
+ ? store.state.search.fieldName
248
+ : null;
143
249
  store.state.generating = true;
144
250
  try {
145
251
  const payload = {
146
252
  tags: store.state.leftPanel.tag ? [store.state.leftPanel.tag] : null,
147
- schema_name: schema_name || null,
253
+ schema_name: activeSchema || null,
254
+ schema_field: activeField || null,
148
255
  route_name: store.state.leftPanel.routeId || null,
149
256
  show_fields: store.state.filter.showFields,
150
- brief: store.state.modeControl.brief,
257
+ brief: store.state.filter.brief,
151
258
  hide_primitive_route: store.state.filter.hidePrimitiveRoute,
152
259
  show_module: store.state.filter.showModule,
153
260
  };
@@ -163,9 +270,9 @@ const app = createApp({
163
270
  graphUI = new GraphUI("#graph", {
164
271
  onSchemaShiftClick: (id) => {
165
272
  if (store.state.graph.schemaKeys.has(id)) {
166
- resetDetailPanels();
167
- store.state.searchDialog.show = true;
168
- store.state.searchDialog.schema = id;
273
+ store.state.search.mode = true;
274
+ store.state.search.schemaName = id;
275
+ onSearch();
169
276
  }
170
277
  },
171
278
  onSchemaClick: (id) => {
@@ -192,29 +299,18 @@ const app = createApp({
192
299
  }
193
300
  }
194
301
 
195
- function showSearchDialog() {
196
- store.state.searchDialog.show = true;
197
- store.state.searchDialog.schema = null;
198
- }
199
-
200
302
  function resetDetailPanels() {
201
303
  store.state.rightDrawer.drawer = false;
202
- store.state.routeDetail.show = false
304
+ store.state.routeDetail.show = false;
203
305
  store.state.schemaDetail.schemaCodeName = "";
204
306
  }
205
307
 
206
308
  async function onReset() {
207
- store.state.leftPanel.tag = null;
208
- store.state.leftPanel._tag = null;
209
- store.state.leftPanel.routeId = null;
210
-
211
- store.state.graph.schemaId = null;
212
-
213
- // state.showFields = "object";
214
- store.state.modeControl.focus = false;
215
- store.state.schemaDetail.schemaCodeName = "";
216
- onGenerate();
217
- syncSelectionToUrl();
309
+ store.state.leftPanel.tag = null;
310
+ store.state.leftPanel._tag = null;
311
+ store.state.leftPanel.routeId = null;
312
+ syncSelectionToUrl()
313
+ onGenerate()
218
314
  }
219
315
 
220
316
  function toggleTag(tagName, expanded = null) {
@@ -223,7 +319,6 @@ const app = createApp({
223
319
  store.state.leftPanel.tag = tagName;
224
320
  store.state.leftPanel.routeId = "";
225
321
 
226
- store.state.modeControl.focus = false;
227
322
  store.state.schemaDetail.schemaCodeName = "";
228
323
  onGenerate();
229
324
  } else {
@@ -231,7 +326,7 @@ const app = createApp({
231
326
  }
232
327
 
233
328
  store.state.rightDrawer.drawer = false;
234
- store.state.routeDetail.show = false
329
+ store.state.routeDetail.show = false;
235
330
  syncSelectionToUrl();
236
331
  }
237
332
 
@@ -242,8 +337,7 @@ const app = createApp({
242
337
  store.state.leftPanel.routeId = routeId;
243
338
  }
244
339
  store.state.rightDrawer.drawer = false;
245
- store.state.routeDetail.show = false
246
- store.state.modeControl.focus = false;
340
+ store.state.routeDetail.show = false;
247
341
  store.state.schemaDetail.schemaCodeName = "";
248
342
  onGenerate();
249
343
  syncSelectionToUrl();
@@ -251,7 +345,7 @@ const app = createApp({
251
345
 
252
346
  function toggleShowModule(val) {
253
347
  store.state.filter.showModule = val;
254
- onGenerate()
348
+ onGenerate();
255
349
  }
256
350
 
257
351
  function toggleShowField(field) {
@@ -260,7 +354,7 @@ const app = createApp({
260
354
  }
261
355
 
262
356
  function toggleBrief(val) {
263
- store.state.modeControl.brief = val;
357
+ store.state.filter.brief = val;
264
358
  onGenerate();
265
359
  }
266
360
 
@@ -293,24 +387,45 @@ const app = createApp({
293
387
  e.preventDefault();
294
388
  }
295
389
 
390
+ watch(
391
+ () => store.state.graph.schemaMap,
392
+ () => {
393
+ rebuildSchemaOptions();
394
+ },
395
+ { deep: false }
396
+ );
397
+
398
+ watch(
399
+ () => store.state.search.schemaName,
400
+ (schemaId) => {
401
+ store.state.search.schemaOptions = allSchemaOptions.value.slice();
402
+ populateFieldOptions(schemaId);
403
+ if (!schemaId) {
404
+ store.state.search.mode = false;
405
+ }
406
+ }
407
+ );
408
+
296
409
  onMounted(async () => {
297
- document.body.classList.remove("app-loading")
410
+ document.body.classList.remove("app-loading");
298
411
  await loadInitial();
299
412
  // Reveal app content only after initial JS/data is ready
300
413
  });
301
414
 
302
415
  return {
303
416
  store,
417
+ onSearch,
418
+ resetSearch,
419
+ filterSearchSchemas,
420
+ onSearchSchemaChange,
304
421
  toggleTag,
305
422
  toggleBrief,
306
423
  toggleHidePrimitiveRoute,
307
424
  selectRoute,
308
425
  onGenerate,
309
426
  onReset,
310
- showSearchDialog,
311
427
  toggleShowField,
312
428
  startDragDrawer,
313
- onFocusChange,
314
429
  toggleShowModule,
315
430
  };
316
431
  },
@@ -323,10 +438,9 @@ if (window.Quasar && typeof window.Quasar.setCssVar === "function") {
323
438
  window.Quasar.setCssVar("primary", "#009485");
324
439
  }
325
440
 
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)
441
+ app.component("schema-code-display", SchemaCodeDisplay); // double click to see node details
442
+ app.component("route-code-display", RouteCodeDisplay); // double click to see route details
443
+ app.component("render-graph", RenderGraph); // for debug, render pasted dot content
444
+ app.component("demo-component", Demo);
331
445
 
332
446
  app.mount("#q-app");
@@ -6,7 +6,7 @@ app.mount(
6
6
  '/voyager',
7
7
  create_voyager(
8
8
  app,
9
- module_color={"tests.service": "purple", "tests.demo": "#00b1cc", "tests": "green"},
9
+ module_color={"tests.service": "purple"},
10
10
  module_prefix="tests.service",
11
11
  swagger_url="/docs",
12
12
  initial_page_policy='first',
@@ -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
- });