fastapi-voyager 0.9.1__py3-none-any.whl → 0.9.3__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.
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.9.1"
2
+ __version__ = "0.9.3"
@@ -3,14 +3,10 @@ const { defineComponent, ref, watch, onMounted } = window.Vue;
3
3
  // Component: RouteCodeDisplay
4
4
  // Props:
5
5
  // routeId: route id key in routeItems
6
- // modelValue: dialog visibility
7
- // routes: object map { id: { id, name, source_code } }
8
6
  export default defineComponent({
9
7
  name: "RouteCodeDisplay",
10
8
  props: {
11
9
  routeId: { type: String, required: true },
12
- modelValue: { type: Boolean, default: false },
13
- routes: { type: Object, default: () => ({}) },
14
10
  },
15
11
  emits: ["close"],
16
12
  setup(props, { emit }) {
@@ -101,32 +97,26 @@ export default defineComponent({
101
97
  }
102
98
  }
103
99
 
104
- watch(
105
- () => props.modelValue,
106
- (v) => {
107
- if (v) load();
108
- }
109
- );
110
100
  watch(
111
101
  () => props.routeId,
112
102
  () => {
113
- if (props.modelValue) load();
103
+ load();
114
104
  }
115
105
  );
116
106
 
117
107
  onMounted(() => {
118
- if (props.modelValue) load();
108
+ load();
119
109
  });
120
110
 
121
111
  return { loading, code, error, close, link };
122
112
  },
123
113
  template: `
124
- <div class="frv-route-code-display" style="border:1px solid #ccc; position:relative; width:50vw; max-width:50vw; height:100%; background:#fff;">
114
+ <div class="frv-route-code-display" style="border:1px solid #ccc; position:relative; background:#fff;">
125
115
  <q-btn dense flat round icon="close" @click="close" aria-label="Close" style="position:absolute; top:6px; right:6px; z-index:10; background:rgba(255,255,255,0.85)" />
126
116
  <div v-if="link" class="q-ml-md q-mt-md" style="padding-top:4px;">
127
117
  <a :href="link" target="_blank" rel="noopener" style="font-size:12px; color:#3b82f6;">Open in VSCode</a>
128
118
  </div>
129
- <div style="padding:40px 16px 16px 16px; height:100%; box-sizing:border-box; overflow:auto;">
119
+ <div style="padding:40px 16px 16px 16px; box-sizing:border-box; overflow:auto;">
130
120
  <div v-if="loading" style="font-family:Menlo, monospace; font-size:12px;">Loading source...</div>
131
121
  <div v-else-if="error" style="color:#c10015; font-family:Menlo, monospace; font-size:12px;">{{ error }}</div>
132
122
  <pre v-else style="margin:0;"><code class="language-python">{{ code }}</code></pre>
@@ -13,23 +13,17 @@ export default defineComponent({
13
13
  name: "SchemaCodeDisplay",
14
14
  props: {
15
15
  schemaName: { type: String, required: true },
16
- modelValue: { type: Boolean, default: false },
17
- schemas: { type: Array, default: () => [] },
16
+ schemas: { type: Object, default: () => ({}) },
18
17
  },
19
- emits: ["close"],
20
18
  setup(props, { emit }) {
21
- const loading = ref(false);
22
19
  const code = ref("");
23
20
  const link = ref("");
24
21
  const error = ref("");
25
22
  const fields = ref([]); // schema fields list
26
23
  const tab = ref("source");
27
24
 
28
- function close() {
29
- emit("close");
30
- }
31
25
 
32
- function highlightLater() {
26
+ async function highlightLater() {
33
27
  // wait a tick for DOM update
34
28
  requestAnimationFrame(() => {
35
29
  try {
@@ -38,6 +32,10 @@ export default defineComponent({
38
32
  ".frv-code-display pre code.language-python"
39
33
  );
40
34
  if (block) {
35
+ // If already highlighted by highlight.js, remove the flag so it can be highlighted again
36
+ if (block.dataset && block.dataset.highlighted) {
37
+ block.removeAttribute("data-highlighted");
38
+ }
41
39
  window.hljs.highlightElement(block);
42
40
  }
43
41
  }
@@ -50,7 +48,6 @@ export default defineComponent({
50
48
  async function loadSource() {
51
49
  if (!props.schemaName) return;
52
50
 
53
- loading.value = true;
54
51
  error.value = null;
55
52
  code.value = "";
56
53
  link.value = "";
@@ -77,7 +74,6 @@ export default defineComponent({
77
74
  } catch (e) {
78
75
  error.value = "Failed to load source";
79
76
  } finally {
80
- loading.value = false;
81
77
  }
82
78
 
83
79
  try {
@@ -99,10 +95,12 @@ export default defineComponent({
99
95
  } catch (e) {
100
96
  error.value = "Failed to load source";
101
97
  } finally {
102
- loading.value = false;
103
98
  }
104
99
 
105
- if (!error.value && tab.value === "source") {
100
+ const schema = props.schemas && props.schemas[props.schemaName];
101
+ fields.value = Array.isArray(schema?.fields) ? schema.fields : [];
102
+
103
+ if (tab.value === "source") {
106
104
  highlightLater();
107
105
  }
108
106
  }
@@ -118,25 +116,21 @@ export default defineComponent({
118
116
  );
119
117
 
120
118
  watch(
121
- () => props.modelValue,
122
- (val) => {
123
- if (val) {
124
- loadSource();
125
- }
126
- }
119
+ () => props.schemaName,
120
+ () => {
121
+ loadSource();
122
+ },
127
123
  );
128
124
 
129
125
  onMounted(() => {
130
- if (props.modelValue) loadSource();
126
+ loadSource();
131
127
  });
132
128
 
133
- return { loading, link, code, error, close, fields, tab };
129
+ return { link, code, error, fields, tab };
134
130
  },
135
131
  template: `
136
- <div class="frv-code-display" style="border: 1px solid #ccc; border-left: none; position:relative; width:40vw; max-width:40vw; height:100%; background:#fff;">
137
- <q-btn dense flat round icon="close" @click="close" aria-label="Close"
138
- style="position:absolute; top:6px; right:6px; z-index:10; background:rgba(255,255,255,0.85)" />
139
- <div v-if="link" class="q-ml-md q-mt-md">
132
+ <div class="frv-code-display" style="border: 1px solid #ccc; border-left: none; position:relative; height:100%; background:#fff;">
133
+ <div class="q-ml-lg q-mt-md">
140
134
  <a :href="link" target="_blank" rel="noopener" style="font-size:12px; color:#3b82f6;">
141
135
  Open in VSCode
142
136
  </a>
@@ -150,8 +144,7 @@ export default defineComponent({
150
144
  </div>
151
145
  <q-separator />
152
146
  <div style="padding:8px 16px 16px 16px; height:75%; box-sizing:border-box; overflow:auto;">
153
- <div v-if="loading" style="font-family:Menlo, monospace; font-size:12px;">Loading source...</div>
154
- <div v-else-if="error" style="color:#c10015; font-family:Menlo, monospace; font-size:12px;">{{ error }}</div>
147
+ <div v-if="error" style="color:#c10015; font-family:Menlo, monospace; font-size:12px;">{{ error }}</div>
155
148
  <template v-else>
156
149
  <div v-show="tab === 'source'">
157
150
  <pre style="margin:0;"><code class="language-python">{{ code }}</code></pre>
@@ -14,7 +14,8 @@ export default defineComponent({
14
14
  name: "SchemaFieldFilter",
15
15
  props: {
16
16
  schemaName: { type: String, default: null }, // external injection triggers auto-query
17
- schemas: { type: Array, default: () => [] }, // externally provided schemas (state.rawSchemasFull or similar)
17
+ // externally provided schemas dict (state.rawSchemasFull): { [id]: schema }
18
+ schemas: { type: Object, default: () => ({}) },
18
19
  },
19
20
  emits: ["queried", "close"],
20
21
  setup(props, { emit }) {
@@ -39,9 +40,11 @@ export default defineComponent({
39
40
  let lastAppliedExternal = null;
40
41
 
41
42
  async function loadSchemas() {
42
- // Refactored: use externally provided props.schemas directly; no network call.
43
+ // Use externally provided props.schemas dict directly; no network call.
43
44
  state.error = null;
44
- state.schemas = Array.isArray(props.schemas) ? props.schemas : [];
45
+ const dict = props.schemas && typeof props.schemas === "object" ? props.schemas : {};
46
+ // Flatten to array for local operations
47
+ state.schemas = Object.values(dict);
45
48
  state.schemaOptions = state.schemas.map((s) => ({
46
49
  label: `${s.name} (${s.id})`,
47
50
  value: s.id,
@@ -69,23 +69,27 @@ export class GraphUI {
69
69
  const set = $();
70
70
  set.push(this);
71
71
  const obj = { set, direction: "bidirectional" };
72
- // Shift+Click to trigger schema detail callback (if provided)
72
+
73
+ const schemaName = event.currentTarget.dataset.name;
73
74
  if (event.shiftKey && self.options.onSchemaClick) {
74
75
  // try data-name or title text
75
- const schemaName = event.currentTarget.dataset.name;
76
76
  if (schemaName) {
77
77
  try {
78
- self.options.onSchemaClick(schemaName);
78
+ self.options.onSchemaShiftClick(schemaName);
79
79
  } catch (e) {
80
- console.warn("onSchemaClick callback failed", e);
80
+ console.warn("onSchemaShiftClick callback failed", e);
81
81
  }
82
82
  }
83
- } else if (event.altKey && self.options.onSchemaAltClick) {
84
- const schemaName = event.currentTarget.dataset.name;
85
- self.options.onSchemaAltClick(schemaName);
86
83
  } else {
87
84
  self.currentSelection = [obj];
88
85
  self._highlight();
86
+ if (schemaName) {
87
+ try {
88
+ self.options.onSchemaClick(schemaName);
89
+ } catch (e) {
90
+ console.warn("onSchemaClick callback failed", e);
91
+ }
92
+ }
89
93
  }
90
94
  });
91
95
 
@@ -55,51 +55,26 @@
55
55
  flex: 1 1 auto;
56
56
  overflow: auto;
57
57
  }
58
+
59
+ .adjust-fit {
60
+ height: calc(100% - 54px);
61
+ }
58
62
  </style>
59
63
  <body>
60
- <div id="q-app" class="column" style="height: 100%">
61
- <div
62
- v-if="state.initializing"
63
- style="
64
- position: fixed;
65
- inset: 0;
66
- display: flex;
67
- align-items: center;
68
- justify-content: center;
69
- z-index: 2000;
70
- background: rgba(255, 255, 255, 0.85);
71
- font-size: 18px;
72
- font-family: Roboto, sans-serif;
73
- color: #444;
74
- "
75
- >
76
- <div style="text-align: center">
77
- <div class="q-mb-sm">Initializing...</div>
78
- <q-spinner color="primary" size="32px" />
79
- </div>
80
- </div>
81
- <div
82
- style="
83
- padding: 8px;
84
- background-color: #fff;
85
- border-bottom: 1px solid #ccc;
86
- color: #211d1d;
87
- flex: 0 0 auto;
88
- "
89
- >
90
- <div class="row items-center q-col-gutter-md">
91
- <div class="col-auto">
64
+ <div id="q-app">
65
+ <q-layout view="hHh lpR fff">
66
+ <q-header bordered class="bg-primary text-white">
67
+ <q-toolbar class="row text-grey-9 bg-white" style="width: 100%">
92
68
  <div
93
- style="width: 282px; margin-left: 8px"
94
- class="text-h6 text-primary"
69
+ class="col-auto text-primary"
70
+ style="font-size: 16px; font-weight: bold"
95
71
  >
96
72
  <q-icon class="q-mr-sm" name="satellite_alt"></q-icon>
97
73
  <span> FastAPI Voyager </span>
98
74
  </div>
99
- </div>
100
- <div class="col-auto">
101
- <div class="column">
75
+ <div class="col-auto" style="font-size: 16px">
102
76
  <q-option-group
77
+ style="margin-left: 140px"
103
78
  v-model="state.showFields"
104
79
  :options="state.fieldOptions"
105
80
  @update:model-value="(val) => toggleShowField(val)"
@@ -109,170 +84,189 @@
109
84
  dense
110
85
  />
111
86
  </div>
112
- </div>
113
-
114
- <!-- <div class="col-auto">
115
- <q-btn-dropdown
116
- class="q-ml-md"
117
- split
118
- :loading="state.generating"
119
- @click="onGenerate"
120
- label="Generate"
121
- outline
122
- >
123
- <q-list>
124
- <q-item clickable v-close-popup @click="onDumpData">
125
- <q-item-section>Dump data</q-item-section>
126
- </q-item>
127
- <q-item clickable v-close-popup @click="openImportDialog">
128
- <q-item-section>Import data</q-item-section>
129
- </q-item>
130
- </q-list>
131
- </q-btn-dropdown>
132
- </div> -->
87
+ <div class="col-auto q-ml-auto">
88
+ <q-toggle
89
+ class="q-mr-md"
90
+ v-model="state.brief"
91
+ label="Brief Mode"
92
+ @update:model-value="(val) => toggleBrief(val)"
93
+ title="skip middle classes, config module_prefix to enable it"
94
+ dense
95
+ />
96
+ </div>
97
+ <div class="col-auto">
98
+ <q-toggle
99
+ v-model="state.hidePrimitiveRoute"
100
+ @update:model-value="(val) => toggleHidePrimitiveRoute(val)"
101
+ label="Hide Primitive"
102
+ title="hide routes return primitive"
103
+ class="q-mr-md"
104
+ dense
105
+ />
106
+ </div>
107
+ <div class="col-auto">
108
+ <q-btn
109
+ outline
110
+ @click="onReset"
111
+ title="may be very slow"
112
+ class="q-mr-md"
113
+ label="Show All"
114
+ />
115
+ </div>
116
+ <div class="col-auto">
117
+ <q-btn
118
+ outline
119
+ icon="search"
120
+ label="Search"
121
+ class="q-mr-md"
122
+ @click="showDialog()"
123
+ />
124
+ </div>
125
+ <div class="col-auto">
126
+ <q-btn
127
+ dense
128
+ round
129
+ flat
130
+ icon="help_outline"
131
+ aria-label="Help"
132
+ class="q-mr-sm"
133
+ >
134
+ <q-tooltip
135
+ anchor="bottom middle"
136
+ self="top middle"
137
+ :offset="[0,8]"
138
+ >
139
+ <div
140
+ class="column items-start text-left"
141
+ style="line-height: 1.4; font-size: 12px"
142
+ >
143
+ <ul>
144
+ <li>scroll to zoom in/out</li>
145
+ <li>
146
+ click node to check highlight related nodes on the
147
+ chart, esc to unselect
148
+ </li>
149
+ <li>
150
+ shift + click to see schema's dependencies without
151
+ unrelated nodes
152
+ </li>
153
+ <li>alt + click to see schema details</li>
154
+ </ul>
155
+ </div>
156
+ </q-tooltip>
157
+ </q-btn>
158
+ </div>
159
+ <!-- <q-toolbar-title class="text-primary row">
160
+ </q-toolbar-title> -->
161
+ </q-toolbar>
162
+ </q-header>
133
163
 
134
- <div class="col-auto q-ml-auto">
135
- <q-toggle
136
- class="q-ml-md"
137
- v-model="state.brief"
138
- label="Brief Mode"
139
- @update:model-value="(val) => toggleBrief(val)"
140
- title="skip middle classes, config module_prefix to enable it"
141
- dense
142
- />
143
- </div>
144
- <div class="col-auto">
145
- <q-toggle
146
- v-model="state.hidePrimitiveRoute"
147
- @update:model-value="(val) => toggleHidePrimitiveRoute(val)"
148
- label="Hide Primitive"
149
- title="hide routes return primitive"
150
- dense
151
- />
152
- </div>
153
- <div class="col-auto">
154
- <q-btn
155
- outline
156
- @click="onReset"
157
- title="may be very slow"
158
- label="Show All"
159
- />
160
- </div>
161
- <div class="col-auto">
162
- <q-btn outline icon="search" label="Search" @click="showDialog()" />
163
- </div>
164
- <div class="col-auto">
164
+ <q-drawer
165
+ v-model="state.detailDrawer"
166
+ width="400"
167
+ side="right"
168
+ overlay
169
+ bordered
170
+ >
171
+ <div style="z-index: 1; position: absolute; left: -17px; top: 9px">
165
172
  <q-btn
166
- dense
173
+ @click="state.detailDrawer = !state.detailDrawer"
167
174
  round
168
- flat
169
- icon="help_outline"
170
- aria-label="Help"
171
- class="q-mr-md"
172
- >
173
- <q-tooltip
174
- anchor="bottom middle"
175
- self="top middle"
176
- :offset="[0,8]"
177
- >
178
- <div
179
- class="column items-start text-left"
180
- style="line-height: 1.4; font-size: 12px"
181
- >
182
- <ul>
183
- <li>scroll to zoom in/out</li>
184
- <li>
185
- click node to check highlight related nodes on the chart,
186
- esc to unselect
187
- </li>
188
- <li>
189
- shift + click to see schema's dependencies without
190
- unrelated nodes
191
- </li>
192
- <li>alt + click to see schema details</li>
193
- </ul>
194
- </div>
195
- </q-tooltip>
196
- </q-btn>
175
+ unelevated
176
+ color="primary"
177
+ dense
178
+ icon="chevron_right"
179
+ />
197
180
  </div>
198
- </div>
199
- </div>
200
- <div style="flex: 1 1 auto; min-height: 0;">
201
- <q-splitter
202
- v-model="state.splitter"
203
- unit="px"
204
- :limits="[200, 800]"
205
- class="fit"
206
- >
207
- <template #before>
208
- <div
209
- id="tag-navigator"
210
- class="column no-wrap"
211
- :style="{
181
+ <!-- move to bottom -->
182
+ <schema-code-display
183
+ :schema-name="schemaCodeName"
184
+ :schemas="state.rawSchemasFull"
185
+ />
186
+ </q-drawer>
187
+
188
+ <q-page-container>
189
+ <q-splitter
190
+ v-model="state.splitter"
191
+ unit="px"
192
+ :limits="[200, 800]"
193
+ class="adjust-fit"
194
+ >
195
+ <template #before>
196
+ <div
197
+ id="tag-navigator"
198
+ class="column no-wrap"
199
+ :style="{
212
200
  borderRight: '1px solid #e0e0e0',
213
201
  backgroundColor: '#fff',
214
202
  minHeight: 0,
215
203
  height: '100%'
216
204
  }"
217
- >
218
- <q-scroll-area class="fit">
219
- <q-list dense separator>
220
- <q-expansion-item
221
- v-for="tag in state.rawTags"
222
- :key="tag.name"
223
- expand-separator
224
- :model-value="state.tag === tag.name"
225
- @update:model-value="(val) => toggleTag(tag.name, val)"
226
- :header-class="state.tag === tag.name ? 'text-primary text-bold' : ''"
227
- content-class="q-pa-none"
228
- >
229
- <template #header>
230
- <div class="row items-center" style="width: 100%">
231
- <div class="row items-end">
232
- <q-icon
233
- class="q-mr-sm"
234
- :name="state.tag == tag.name ? 'folder' : 'folder_open'"
235
- ></q-icon>
236
- <span>{{ tag.name }}</span>
205
+ >
206
+ <q-scroll-area class="fit">
207
+ <q-list dense separator>
208
+ <q-expansion-item
209
+ v-for="tag in state.rawTags"
210
+ :key="tag.name"
211
+ expand-separator
212
+ :model-value="state.tag === tag.name"
213
+ @update:model-value="(val) => toggleTag(tag.name, val)"
214
+ :header-class="state.tag === tag.name ? 'text-primary text-bold' : ''"
215
+ content-class="q-pa-none"
216
+ >
217
+ <template #header>
218
+ <div class="row items-center" style="width: 100%">
219
+ <div class="row items-end">
220
+ <q-icon
221
+ class="q-mr-sm"
222
+ :name="state.tag == tag.name ? 'folder' : 'folder_open'"
223
+ ></q-icon>
224
+ <span>{{ tag.name }}</span>
225
+ </div>
237
226
  </div>
238
- </div>
239
- </template>
227
+ </template>
240
228
 
241
- <q-list separator>
242
- <q-item
243
- v-for="route in (state.hidePrimitiveRoute ? tag.routes.filter(r => !r.is_primitive) :tag.routes || [])"
244
- :key="route.id"
245
- clickable
246
- v-ripple
247
- :active="state.routeId === route.id"
248
- active-class=""
249
- @click="selectRoute(route.id)"
250
- >
251
- <q-item-section>
252
- <span class="q-ml-lg" style="white-space: nowrap">
253
- <q-icon class="q-mr-sm" name="data_object"></q-icon>
254
- {{ route.name }}
255
- </span>
256
- </q-item-section>
257
- </q-item>
258
- <q-item v-if="!tag.routes || tag.routes.length === 0" dense>
259
- <q-item-section class="text-grey-6"
260
- >No routes</q-item-section
229
+ <q-list separator>
230
+ <q-item
231
+ v-for="route in (state.hidePrimitiveRoute ? tag.routes.filter(r => !r.is_primitive) :tag.routes || [])"
232
+ :key="route.id"
233
+ clickable
234
+ v-ripple
235
+ :active="state.routeId === route.id"
236
+ active-class=""
237
+ @click="selectRoute(route.id)"
261
238
  >
262
- </q-item>
263
- </q-list>
264
- </q-expansion-item>
265
- </q-list>
266
- </q-scroll-area>
267
- </div>
268
- </template>
269
-
270
- <template #after>
271
- <div id="graph" class="fit"></div>
272
- </template>
273
- </q-splitter>
274
- </div>
239
+ <q-item-section>
240
+ <span class="q-ml-lg" style="white-space: nowrap">
241
+ <q-icon
242
+ class="q-mr-sm"
243
+ name="data_object"
244
+ ></q-icon>
245
+ {{ route.name }}
246
+ </span>
247
+ </q-item-section>
248
+ </q-item>
249
+ <q-item
250
+ v-if="!tag.routes || tag.routes.length === 0"
251
+ dense
252
+ >
253
+ <q-item-section class="text-grey-6"
254
+ >No routes</q-item-section
255
+ >
256
+ </q-item>
257
+ </q-list>
258
+ </q-expansion-item>
259
+ </q-list>
260
+ </q-scroll-area>
261
+ </div>
262
+ </template>
275
263
 
264
+ <template #after>
265
+ <div id="graph" class="fit"></div>
266
+ </template>
267
+ </q-splitter>
268
+ </q-page-container>
269
+ </q-layout>
276
270
  <!-- Detail Dialog -->
277
271
  <q-dialog v-model="showDetail" :persistent="true" :maximized="true">
278
272
  <detail-dialog
@@ -296,36 +290,10 @@
296
290
  />
297
291
  </q-dialog>
298
292
 
299
- <!-- Schema Source Code Dialog (Ctrl + Click) -->
300
- <q-dialog
301
- v-model="showSchemaCode"
302
- :maximized="true"
303
- :persistent="false"
304
- position="left"
305
- :seamless="false"
306
- >
307
- <schema-code-display
308
- :schema-name="schemaCodeName"
309
- :model-value="showSchemaCode"
310
- :schemas="state.rawSchemasFull"
311
- @close="showSchemaCode = false"
312
- />
313
- </q-dialog>
314
-
315
- <!-- Route Source Code Dialog (Alt + Click on route) -->
316
- <q-dialog
317
- v-model="showRouteCode"
318
- :maximized="true"
319
- :persistent="false"
320
- position="right"
321
- :seamless="false"
322
- >
323
- <route-code-display
324
- :route-id="routeCodeId"
325
- :model-value="showRouteCode"
326
- :routes="state.routeItems"
327
- @close="showRouteCode = false"
328
- />
293
+ <q-dialog v-model="showRouteDetail" position="bottom">
294
+ <q-card style="width: 1100px; max-width: 1100px">
295
+ <route-code-display :route-id="routeCodeId" @close="showRouteDetail=false" />
296
+ </q-card>
329
297
  </q-dialog>
330
298
 
331
299
  <!-- Dump Core Data Dialog -->
@@ -9,16 +9,12 @@ const app = createApp({
9
9
  setup() {
10
10
  const state = reactive({
11
11
  // options and selections
12
- tag: null,
13
- tagOptions: [], // array of strings
14
- routeId: null,
15
- routeOptions: [], // [{ label, value }]
16
- schemaId: null,
17
- schemaOptions: [], // [{ label, value }]
18
- routeItems: {}, // { id: { label, value } }
12
+ tag: null, // picked tag
13
+ routeId: null, // picked route
14
+ schemaId: null, // picked schema
19
15
  showFields: "object",
20
16
  fieldOptions: [
21
- { label: "No fields", value: "single" },
17
+ { label: "No field", value: "single" },
22
18
  { label: "Object fields", value: "object" },
23
19
  { label: "All fields", value: "all" },
24
20
  ],
@@ -26,17 +22,16 @@ const app = createApp({
26
22
  hidePrimitiveRoute: false,
27
23
  generating: false,
28
24
  rawTags: [], // [{ name, routes: [{ id, name }] }]
29
- rawSchemas: [], // [{ name, id }]
30
- rawSchemasFull: [], // full objects with source_code & fields
25
+ rawSchemas: new Set(), // [{ name, id }]
26
+ rawSchemasFull: {}, // full schemas dict: { [schema.id]: schema }
31
27
  initializing: true,
32
28
  // Splitter size (left panel width in px)
33
29
  splitter: 300,
30
+ detailDrawer: false
34
31
  });
32
+
35
33
  const showDetail = ref(false);
36
34
  const showSchemaFieldFilter = ref(false);
37
- const showSchemaCode = ref(false);
38
- const showRouteCode = ref(false);
39
- // Dump/Import dialogs and rendered graph dialog
40
35
  const showDumpDialog = ref(false);
41
36
  const dumpJson = ref("");
42
37
  const showImportDialog = ref(false);
@@ -47,6 +42,7 @@ const app = createApp({
47
42
  const schemaFieldFilterSchema = ref(null); // external schemaName for schema-field-filter
48
43
  const schemaCodeName = ref("");
49
44
  const routeCodeId = ref("");
45
+ const showRouteDetail = ref(false)
50
46
 
51
47
  function openDetail() {
52
48
  showDetail.value = true;
@@ -55,47 +51,18 @@ const app = createApp({
55
51
  showDetail.value = false;
56
52
  }
57
53
 
58
- function onFilterTags(val, update) {
59
- const normalized = (val || "").toLowerCase();
60
- update(() => {
61
- if (!normalized) {
62
- state.tagOptions = state.rawTags.map((t) => t.name);
63
- return;
64
- }
65
- state.tagOptions = state.rawTags
66
- .map((t) => t.name)
67
- .filter((n) => n.toLowerCase().includes(normalized));
68
- });
69
- }
70
-
71
- function onFilterSchemas(val, update) {
72
- const normalized = (val || "").toLowerCase();
73
- update(() => {
74
- const makeLabel = (s) => `${s.name} (${s.id})`;
75
- let list = state.rawSchemas.map((s) => ({
76
- label: makeLabel(s),
77
- value: s.id,
78
- }));
79
- if (normalized) {
80
- list = list.filter((opt) =>
81
- opt.label.toLowerCase().includes(normalized)
82
- );
83
- }
84
- state.schemaOptions = list;
85
- });
86
- }
87
-
88
54
  async function loadInitial() {
89
55
  state.initializing = true;
90
56
  try {
91
57
  const res = await fetch("dot");
92
58
  const data = await res.json();
93
59
  state.rawTags = Array.isArray(data.tags) ? data.tags : [];
94
- state.rawSchemasFull = Array.isArray(data.schemas) ? data.schemas : [];
95
- state.rawSchemas = state.rawSchemasFull.map((s) => ({
96
- name: s.name,
97
- id: s.id,
98
- }));
60
+ const schemasArr = Array.isArray(data.schemas) ? data.schemas : [];
61
+ // Build dict keyed by id for faster lookups and simpler prop passing
62
+ state.rawSchemasFull = Object.fromEntries(
63
+ schemasArr.map((s) => [s.id, s])
64
+ );
65
+ state.rawSchemas = new Set(Object.keys(state.rawSchemasFull));
99
66
  state.routeItems = data.tags
100
67
  .map((t) => t.routes)
101
68
  .flat()
@@ -104,13 +71,7 @@ const app = createApp({
104
71
  return acc;
105
72
  }, {});
106
73
 
107
- state.tagOptions = state.rawTags.map((t) => t.name);
108
- state.schemaOptions = state.rawSchemas.map((s) => ({
109
- label: `${s.name} (${s.id})`,
110
- value: s.id,
111
- }));
112
74
  // default route options placeholder
113
- state.routeOptions = [];
114
75
  } catch (e) {
115
76
  console.error("Initial load failed", e);
116
77
  } finally {
@@ -139,23 +100,20 @@ const app = createApp({
139
100
 
140
101
  // create graph instance once
141
102
  const graphUI = new GraphUI("#graph", {
142
- onSchemaClick: (id) => {
143
- if (state.rawSchemas.find((s) => s.id === id)) {
103
+ onSchemaShiftClick: (id) => {
104
+ if (state.rawSchemas.has(id)) {
144
105
  schemaFieldFilterSchema.value = id;
145
106
  showSchemaFieldFilter.value = true;
146
107
  }
147
108
  },
148
- onSchemaAltClick: (id) => {
149
- // priority: schema id; else route id
150
- if (state.rawSchemas.find((s) => s.id === id)) {
109
+ onSchemaClick: (id) => {
110
+ if (state.rawSchemas.has(id)) {
151
111
  schemaCodeName.value = id;
152
- showSchemaCode.value = true;
153
- return;
112
+ state.detailDrawer = true
154
113
  }
155
114
  if (id in state.routeItems) {
156
115
  routeCodeId.value = id;
157
- showRouteCode.value = true;
158
- return;
116
+ showRouteDetail.value = true
159
117
  }
160
118
  },
161
119
  });
@@ -282,19 +240,16 @@ const app = createApp({
282
240
  toggleBrief,
283
241
  toggleHidePrimitiveRoute,
284
242
  selectRoute,
285
- onFilterTags,
286
- onFilterSchemas,
287
243
  onGenerate,
288
244
  onReset,
289
245
  showDetail,
246
+ showRouteDetail,
290
247
  openDetail,
291
248
  closeDetail,
292
249
  schemaName,
293
250
  showSchemaFieldFilter,
294
251
  schemaFieldFilterSchema,
295
252
  showDialog,
296
- showSchemaCode,
297
- showRouteCode,
298
253
  schemaCodeName,
299
254
  routeCodeId,
300
255
  // dump/import
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-voyager
3
- Version: 0.9.1
3
+ Version: 0.9.3
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
@@ -27,18 +27,15 @@ Description-Content-Type: text/markdown
27
27
 
28
28
  [![pypi](https://img.shields.io/pypi/v/fastapi-voyager.svg)](https://pypi.python.org/pypi/fastapi-voyager)
29
29
  ![Python Versions](https://img.shields.io/pypi/pyversions/fastapi-voyager)
30
-
31
- <p align="center"><img src="./voyager.jpg" alt="" /></p>
32
-
33
-
34
- [![IMAGE ALT TEXT](http://img.youtube.com/vi/PGlbQq1M-n8/0.jpg)](https://www.youtube.com/watch?v=PGlbQq1M-n8 "FastAPI Voyager")
30
+ [![PyPI Downloads](https://static.pepy.tech/badge/fastapi-voyager/month)](https://pepy.tech/projects/fastapi-voyager)
35
31
 
36
32
 
37
- > This repo is still in early stage, currently it supports pydantic v2 only, previous name: fastapi-router-viz
33
+ > This repo is still in early stage, it supports pydantic v2 only
38
34
 
39
- Inspect your API interactively
35
+ Inspect your API interactively!
40
36
 
41
- <img width="1480" height="648" alt="image" src="https://github.com/user-attachments/assets/a6ccc9f1-cf06-493a-b99b-eb07767564bd" />
37
+ <p align="center"><img src="./voyager.jpg" alt="" /></p>
38
+ <p align="center"><a target="_blank" rel="" href="https://www.youtube.com/watch?v=PGlbQq1M-n8"><img src="http://img.youtube.com/vi/PGlbQq1M-n8/0.jpg" alt="" style="max-width: 100%;"></a></p>
42
39
 
43
40
  ## Installation
44
41
 
@@ -52,11 +49,6 @@ uv add fastapi-voyager
52
49
  voyager -m path.to.your.app.module --server
53
50
  ```
54
51
 
55
- ## Dependencies
56
-
57
- - FastAPI
58
- - [pydantic-resolve](https://github.com/allmonday/pydantic-resolve)
59
-
60
52
 
61
53
  ## Feature
62
54
 
@@ -79,9 +71,9 @@ voyager -m tests.demo
79
71
  ```
80
72
 
81
73
  ### generate the graph
82
- after initialization, pick tag, rotue (optional) and click `generate`.
74
+ after initialization, pick tag, rotue to render graph
83
75
 
84
- <img width="1919" height="898" alt="image" style="border: 1px solid #aaa" src="https://github.com/user-attachments/assets/05e321d0-49f3-4af6-a7c7-f4c9c6b1dbfd" />
76
+ <img width="1628" height="765" alt="image" src="https://github.com/user-attachments/assets/b4712f82-e754-453b-aa69-24c932b8f48f" />
85
77
 
86
78
  ### highlight
87
79
  click a node to highlight it's upperstream and downstream nodes. figure out the related models of one page, or homw many pages are related with one model.
@@ -89,9 +81,9 @@ click a node to highlight it's upperstream and downstream nodes. figure out the
89
81
  <img width="1485" height="616" alt="image" style="border: 1px solid #aaa" src="https://github.com/user-attachments/assets/70c4095f-86c7-45da-a6f0-fd41ac645813" />
90
82
 
91
83
  ### filter related nodes
92
- `shift` click a node to check related node, pick a field to narrow the result.
84
+ `shift` click a node to check related node, pick a field to narrow the result, picked node is marked as red.
93
85
 
94
- <img width="1917" height="800" alt="image" style="border: 1px solid #aaa" src="https://github.com/user-attachments/assets/e770dc70-f293-49e1-bcd7-d8dffa15d9ea" />
86
+ <img width="1423" height="552" alt="image" src="https://github.com/user-attachments/assets/468a058d-afa1-4601-a7c5-c6aad6a8a557" />
95
87
 
96
88
  ### view source code
97
89
  `alt` click a node to show source code or open file in vscode.
@@ -168,7 +160,24 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
168
160
 
169
161
  ## Plan before v1.0
170
162
 
171
- ### features:
163
+
164
+ ### backlog
165
+ - [ ] user can generate nodes/edges manually and connect to generated ones
166
+ - [ ] add owner
167
+ - [ ] add extra info for schema
168
+ - [ ] display standard ER diagram `hard`
169
+ - [ ] display potential invalid links
170
+ - [ ] support dataclass (pending)
171
+
172
+ ### in analysis
173
+ - [ ] click field to highlight links
174
+ - [ ] animation effect for edges
175
+ - [ ] customrized right click panel
176
+ - [ ] show own dependencies
177
+ - [ ] clean up fe code
178
+
179
+ ### plan:
180
+ #### <0.9:
172
181
  - [x] group schemas by module hierarchy
173
182
  - [x] module-based coloring via Analytics(module_color={...})
174
183
  - [x] view in web browser
@@ -196,26 +205,6 @@ or you can open router_viz.dot with vscode extension `graphviz interactive previ
196
205
  - [x] fix duplicated link from class and parent class, it also break clicking highlight
197
206
  - [x] refactor: abstract render module
198
207
 
199
- ### backlog
200
- - [ ] user can generate nodes/edges manually and connect to generated ones
201
- - [ ] add owner
202
- - [ ] add extra info for schema
203
- - [ ] fixed left/right bar show field information
204
- - [ ] display standard ER diagram `hard`
205
- - [ ] display potential invalid links
206
-
207
- bugs & non feature:
208
- - [ ] add tests
209
- - [ ] support dataclass (pending)
210
-
211
- ### in analysis
212
- - [ ] click field to highlight links
213
- - [ ] animation effect for edges
214
- - [ ] customrized right click panel
215
- - [ ] show own dependencies
216
-
217
- ### plan:
218
-
219
208
  #### 0.9
220
209
  - [x] refactor: server.py
221
210
  - [x] rename create_app_with_fastapi -> create_voyager
@@ -223,19 +212,36 @@ bugs & non feature:
223
212
  - [x] improve initialization time cost
224
213
  - [x] query route / schema info through realtime api
225
214
  - [x] adjust fe
215
+ - [x] adjust layout (0.9.3)
216
+ - [x] show field detail in right panel
217
+ - [x] show route info in bottom
226
218
 
227
219
  #### 0.10
228
- - [ ] logging information
229
- - [ ] open route in swagger
220
+ - [ ] support opening route in swagger
230
221
  - config docs path
231
222
  - [ ] add http method for route
232
223
  - [ ] enable/disable module cluster (may save space)
224
+ - [ ] logging information
225
+ - [ ] add tests
226
+ - [ ] hide brief mode if not configured
227
+ - [ ] optimize static resource
228
+ - [ ] show route count in tag expansion item
229
+ - [ ] route list show have a max height to trigger scrollable
230
+ - [ ] fix layout issue when rendering huge graph
233
231
 
234
232
  #### 0.11
233
+ - [ ] improve user experience
234
+ - double click to show detail
235
+ - improve search dialog
236
+
237
+ #### 0.12
235
238
  - [ ] integration with pydantic-resolve
236
239
  - [ ] show hint for resolve, post fields
237
240
  - [ ] display loader as edges
238
241
 
242
+ #### 0.13
243
+ - [ ] config release pipeline
244
+ - [ ]
239
245
 
240
246
  ## Using with pydantic-resolve
241
247
 
@@ -247,13 +253,25 @@ pydantic-resolve's @ensure_subset decorator is helpful to pick fields from `sour
247
253
 
248
254
  ## Credits
249
255
 
250
- - https://apis.guru/graphql-voyager/, for inspiration.
251
- - https://github.com/tintinweb/vscode-interactive-graphviz, for web visualization.
256
+ - https://apis.guru/graphql-voyager/, thanks for inspiration.
257
+ - https://github.com/tintinweb/vscode-interactive-graphviz, thanks for web visualization.
258
+
259
+
260
+ ## Dependencies
261
+
262
+ - FastAPI
263
+ - [pydantic-resolve](https://github.com/allmonday/pydantic-resolve)
264
+ - Quasar
252
265
 
253
266
 
254
267
  ## Changelog
255
268
 
256
269
  - 0.9:
270
+ - 0.9.3:
271
+ - enhancement: better UI
272
+ - 0.9.2:
273
+ - fix: missing fields in schema detail panel
274
+ - optimization: clean up fe codes.
257
275
  - 0.9.1:
258
276
  - api change: from `create_app_with_fastapi` to `create_voyager`, and expose as `from fastapi_voyager import create_voyager`
259
277
  - optimization: lazy load vscode link and source code, speed up the initialization.
@@ -6,19 +6,19 @@ fastapi_voyager/render.py,sha256=qy3g1Rz1s8XkuR_6n1Q1YPwy_oMOjWjNswTHQjdz4N0,776
6
6
  fastapi_voyager/server.py,sha256=pg-LHDj4yU0usDA1b2X2Kt2_OCrexFA2G9ifGxb52Uc,6196
7
7
  fastapi_voyager/type.py,sha256=pWYKmgb9e0W_JeD7k54Mr2lxUZV_Ir9TNpewGRwHyHQ,1629
8
8
  fastapi_voyager/type_helper.py,sha256=hjBC4E0tgBpQDlYxGg74uK07SXjsrAgictEETJfIpYM,9231
9
- fastapi_voyager/version.py,sha256=jjAALUE-D-m7hgihxqGZyrZQPe4_Ch5MQPu89WFuVmw,48
9
+ fastapi_voyager/version.py,sha256=Lt2a1ZjlXG3QcL31iJHgzvRFDSTSsCXmwjQNWNdNUMY,48
10
10
  fastapi_voyager/voyager.py,sha256=pXvA3ye5Jq6aJ6YrZFbTHjJ9MzdydqWllIgY8RFK_4A,10793
11
- fastapi_voyager/web/graph-ui.js,sha256=0hqMCzsPJfhgWDuwiXVXaGQL-_7urT_zryo4sXMN8jQ,4209
11
+ fastapi_voyager/web/graph-ui.js,sha256=QYJXHOH4EWF2jbcX3usTKv4tNody0RjpnVz-eh0UKkU,4185
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=AnsqXfzDrUPO12EWtiT_XJdJ6Kx3qBsRn1IfX393G48,14298
14
+ fastapi_voyager/web/index.html,sha256=YtVoDRrgz64E7IHpcPJBzUb1DI1H5P7a-3ziTx5BHYA,13735
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=p8akwqNf9sDM71xio0SN-3JznUOIksoy9SGRiqAeg2s,9459
17
+ fastapi_voyager/web/vue-main.js,sha256=kQJpcQ2ENzA90v1XJ1P2bNGfeFC4drTGoG5dNhQKQqc,8105
18
18
  fastapi_voyager/web/component/render-graph.js,sha256=e8Xgh2Kl-nYU0P1gstEmAepCgFnk2J6UdxW8TlMafGs,2322
19
- fastapi_voyager/web/component/route-code-display.js,sha256=4VN88n_7FYHxvruP5AbgTdF6Lyj6v_7Ocqy8zOVMjVA,4065
20
- fastapi_voyager/web/component/schema-code-display.js,sha256=_Okkr8ceJsA_Xngyj_GpxJqdN01GzCjJTeAZamSA9Ks,6514
21
- fastapi_voyager/web/component/schema-field-filter.js,sha256=1llPkN8w-RrBPHVtjjMcObndaqPnmZ7e4iU6dMJMIaE,6224
19
+ fastapi_voyager/web/component/route-code-display.js,sha256=8NJPPjNRUC21gjpY8XYEQs4RBbhX1pCiqEhJp39ku6k,3678
20
+ fastapi_voyager/web/component/schema-code-display.js,sha256=B-NOAKQIDORvFlKrsWNFAMGweXYBXrpg6AjKt4TFQFI,6256
21
+ fastapi_voyager/web/component/schema-field-filter.js,sha256=PR8d2_u1UiSLrS5zqAvY2UR8LbvjBqiUDt71tm1DJTs,6345
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.9.1.dist-info/METADATA,sha256=7yR9Wj87QH6f2kxA4QiqDnwWJPQ2-rGOhpC6ZYYfHfE,8914
30
- fastapi_voyager-0.9.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
- fastapi_voyager-0.9.1.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
32
- fastapi_voyager-0.9.1.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
33
- fastapi_voyager-0.9.1.dist-info/RECORD,,
29
+ fastapi_voyager-0.9.3.dist-info/METADATA,sha256=m7vs-SegP6z60WrAwoMBV-fqyrUwlg08_EktecMzcSM,9490
30
+ fastapi_voyager-0.9.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
+ fastapi_voyager-0.9.3.dist-info/entry_points.txt,sha256=pEIKoUnIDXEtdMBq8EmXm70m16vELIu1VPz9-TBUFWM,53
32
+ fastapi_voyager-0.9.3.dist-info/licenses/LICENSE,sha256=lNVRR3y_bFVoFKuK2JM8N4sFaj3m-7j29kvL3olFi5Y,1067
33
+ fastapi_voyager-0.9.3.dist-info/RECORD,,