fastapi-voyager 0.15.1__tar.gz → 0.15.3__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.
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/.github/workflows/publish.yml +9 -1
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/PKG-INFO +1 -1
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/docs/changelog.md +6 -2
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/version.py +1 -1
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/index.html +38 -1
- fastapi_voyager-0.15.3/src/fastapi_voyager/web/store.js +529 -0
- fastapi_voyager-0.15.3/src/fastapi_voyager/web/vue-main.js +331 -0
- fastapi_voyager-0.15.1/src/fastapi_voyager/web/store.js +0 -115
- fastapi_voyager-0.15.1/src/fastapi_voyager/web/vue-main.js +0 -588
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/.githooks/README.md +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/.githooks/pre-commit +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/.gitignore +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/.prettierignore +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/.prettierrc +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/.python-version +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/CONTRIBUTING.md +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/LICENSE +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/README.md +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/docs/claude/0_REFACTORING_RENDER_NOTES.md +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/docs/idea.md +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/package-lock.json +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/pyproject.toml +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/release.md +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/setup-hooks.sh +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/__init__.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/cli.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/er_diagram.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/filter.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/module.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/pydantic_resolve_util.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/render.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/render_style.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/server.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/cluster.j2 +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/cluster_container.j2 +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/digraph.j2 +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/er_diagram.j2 +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/link.j2 +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/route_node.j2 +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/schema_node.j2 +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/dot/tag_node.j2 +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/html/colored_text.j2 +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/html/pydantic_meta.j2 +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/html/schema_field_row.j2 +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/html/schema_header.j2 +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/templates/html/schema_table.j2 +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/type.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/type_helper.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/voyager.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/component/demo.js +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/component/render-graph.js +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/graph-ui.js +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/quasar.min.css +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/src/fastapi_voyager/web/quasar.min.js +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/__init__.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/demo.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/demo_anno.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/programatic.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/service/__init__.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/service/schema/__init__.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/service/schema/base_entity.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/service/schema/extra.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/service/schema/schema.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/test_analysis.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/test_filter.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/test_generic.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/test_import.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/test_module.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/test_resolve_util_impl.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/tests/test_type_helper.py +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/uv.lock +0 -0
- {fastapi_voyager-0.15.1 → fastapi_voyager-0.15.3}/voyager.jpg +0 -0
|
@@ -11,7 +11,7 @@ jobs:
|
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
12
|
|
|
13
13
|
permissions:
|
|
14
|
-
contents:
|
|
14
|
+
contents: write
|
|
15
15
|
|
|
16
16
|
steps:
|
|
17
17
|
- name: Checkout repository
|
|
@@ -30,3 +30,11 @@ jobs:
|
|
|
30
30
|
|
|
31
31
|
- name: Publish to PyPI
|
|
32
32
|
run: uv publish --token ${{ secrets.PYPI_PUBLISHER }}
|
|
33
|
+
|
|
34
|
+
- name: Create GitHub Release
|
|
35
|
+
uses: softprops/action-gh-release@v2
|
|
36
|
+
with:
|
|
37
|
+
generate_release_notes: true
|
|
38
|
+
files: |
|
|
39
|
+
dist/*.tar.gz
|
|
40
|
+
dist/*.whl
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.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
|
|
@@ -161,9 +161,13 @@
|
|
|
161
161
|
- [x] refactor er diagram renderer
|
|
162
162
|
- [x] fix error in search function
|
|
163
163
|
- 0.15.2
|
|
164
|
+
- [x] fix resetSearch issue: fail to go back previous tag/router after reset.
|
|
165
|
+
- [x] left panel can be toggled.
|
|
166
|
+
- 0.15.3
|
|
167
|
+
- [x] refactor vue-main.js, move methods to store
|
|
168
|
+
- [x] optimize search flow
|
|
169
|
+
- 0.15.4
|
|
164
170
|
- [ ] add tests
|
|
165
|
-
- [ ] left panel can be toggled.
|
|
166
|
-
- [ ] refactor vue-main.js, move methods to store
|
|
167
171
|
|
|
168
172
|
## 1.0, release
|
|
169
173
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.15.
|
|
2
|
+
__version__ = "0.15.3"
|
|
@@ -101,6 +101,30 @@
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/* Tag Navigator Collapse Button */
|
|
105
|
+
.tag-navigator-collapse-btn-right {
|
|
106
|
+
position: absolute;
|
|
107
|
+
bottom: 8px;
|
|
108
|
+
left: 8px;
|
|
109
|
+
width: 32px;
|
|
110
|
+
height: 32px;
|
|
111
|
+
border-radius: 50%;
|
|
112
|
+
background: #009485;
|
|
113
|
+
color: white;
|
|
114
|
+
border: 2px solid white;
|
|
115
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
z-index: 100;
|
|
121
|
+
transition: all 0.2s ease;
|
|
122
|
+
}
|
|
123
|
+
.tag-navigator-collapse-btn-right:hover {
|
|
124
|
+
background: #007a6d;
|
|
125
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
|
|
126
|
+
}
|
|
127
|
+
|
|
104
128
|
/* App boot loading overlay & gating */
|
|
105
129
|
#app-loading-overlay {
|
|
106
130
|
position: fixed;
|
|
@@ -352,7 +376,7 @@
|
|
|
352
376
|
<q-splitter
|
|
353
377
|
v-model="store.state.leftPanel.width"
|
|
354
378
|
unit="px"
|
|
355
|
-
:limits="store.state.mode === 'voyager' ? [
|
|
379
|
+
:limits="store.state.mode === 'voyager' ? [0, 800] : [0, 0]"
|
|
356
380
|
class="adjust-fit"
|
|
357
381
|
>
|
|
358
382
|
<template #before>
|
|
@@ -509,6 +533,19 @@
|
|
|
509
533
|
/>
|
|
510
534
|
</div>
|
|
511
535
|
</div>
|
|
536
|
+
|
|
537
|
+
<!-- Collapse toggle button for tag navigator - at bottom-left of right panel -->
|
|
538
|
+
<div
|
|
539
|
+
v-show="store.state.mode === 'voyager'"
|
|
540
|
+
class="tag-navigator-collapse-btn-right"
|
|
541
|
+
@click="toggleTagNavigatorCollapse"
|
|
542
|
+
:title="store.state.leftPanel.collapsed ? 'Expand tag navigator' : 'Collapse tag navigator'"
|
|
543
|
+
>
|
|
544
|
+
<q-icon
|
|
545
|
+
:name="store.state.leftPanel.collapsed ? 'chevron_right' : 'chevron_left'"
|
|
546
|
+
size="18px"
|
|
547
|
+
/>
|
|
548
|
+
</div>
|
|
512
549
|
</div>
|
|
513
550
|
</template>
|
|
514
551
|
</q-splitter>
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
const { reactive } = window.Vue
|
|
2
|
+
|
|
3
|
+
const state = reactive({
|
|
4
|
+
version: "",
|
|
5
|
+
config: {
|
|
6
|
+
initial_page_policy: "first",
|
|
7
|
+
has_er_diagram: false,
|
|
8
|
+
enable_pydantic_resolve_meta: false,
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
mode: "voyager", // voyager / er-diagram
|
|
12
|
+
|
|
13
|
+
previousTagRoute: {
|
|
14
|
+
// Store the last non-search tag/route selection for restoration when clearing search
|
|
15
|
+
// Used by resetSearch to return to the state before entering search mode
|
|
16
|
+
hasValue: false,
|
|
17
|
+
tag: null,
|
|
18
|
+
routeId: null,
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
swagger: {
|
|
22
|
+
url: "",
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
rightDrawer: {
|
|
26
|
+
drawer: false,
|
|
27
|
+
width: 300,
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
fieldOptions: [
|
|
31
|
+
{ label: "No field", value: "single" },
|
|
32
|
+
{ label: "Object fields", value: "object" },
|
|
33
|
+
{ label: "All fields", value: "all" },
|
|
34
|
+
],
|
|
35
|
+
|
|
36
|
+
// tags and routes
|
|
37
|
+
leftPanel: {
|
|
38
|
+
width: 300,
|
|
39
|
+
previousWidth: 300,
|
|
40
|
+
tags: null,
|
|
41
|
+
fullTagsCache: null, // Cache for full tags (before search)
|
|
42
|
+
tag: null,
|
|
43
|
+
_tag: null,
|
|
44
|
+
routeId: null,
|
|
45
|
+
collapsed: false,
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
graph: {
|
|
49
|
+
schemaId: null,
|
|
50
|
+
schemaKeys: new Set(),
|
|
51
|
+
schemaMap: {},
|
|
52
|
+
routeItems: [],
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// schema options, schema, fields
|
|
56
|
+
search: {
|
|
57
|
+
mode: false,
|
|
58
|
+
invisible: false,
|
|
59
|
+
schemaName: null,
|
|
60
|
+
fieldName: null,
|
|
61
|
+
schemaOptions: [],
|
|
62
|
+
fieldOptions: [],
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// cache all schema options for filtering
|
|
66
|
+
allSchemaOptions: [],
|
|
67
|
+
|
|
68
|
+
// route information
|
|
69
|
+
routeDetail: {
|
|
70
|
+
show: false,
|
|
71
|
+
routeCodeId: "",
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// schema information
|
|
75
|
+
schemaDetail: {
|
|
76
|
+
show: false,
|
|
77
|
+
schemaCodeName: "",
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
searchDialog: {
|
|
81
|
+
show: false,
|
|
82
|
+
schema: null,
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// global status
|
|
86
|
+
status: {
|
|
87
|
+
generating: false,
|
|
88
|
+
loading: false,
|
|
89
|
+
initializing: true,
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// brief, hide primitive ...
|
|
93
|
+
modeControl: {
|
|
94
|
+
focus: false, // control the schema param
|
|
95
|
+
briefModeEnabled: false, // show brief mode toggle
|
|
96
|
+
pydanticResolveMetaEnabled: false, // show pydantic resolve meta toggle
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// api filters
|
|
100
|
+
filter: {
|
|
101
|
+
hidePrimitiveRoute: false,
|
|
102
|
+
showFields: "object",
|
|
103
|
+
brief: false,
|
|
104
|
+
showModule: false,
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const getters = {
|
|
109
|
+
/**
|
|
110
|
+
* Find tag name by route ID
|
|
111
|
+
* Used to determine which tag a route belongs to
|
|
112
|
+
*/
|
|
113
|
+
findTagByRoute(routeId) {
|
|
114
|
+
return (
|
|
115
|
+
state.leftPanel.tags.find((tag) => (tag.routes || []).some((route) => route.id === routeId))
|
|
116
|
+
?.name || null
|
|
117
|
+
)
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const actions = {
|
|
122
|
+
/**
|
|
123
|
+
* Read tag and route from URL query parameters
|
|
124
|
+
* @returns {{ tag: string|null, route: string|null }}
|
|
125
|
+
*/
|
|
126
|
+
readQuerySelection() {
|
|
127
|
+
if (typeof window === "undefined") {
|
|
128
|
+
return { tag: null, route: null }
|
|
129
|
+
}
|
|
130
|
+
const params = new URLSearchParams(window.location.search)
|
|
131
|
+
return {
|
|
132
|
+
tag: params.get("tag") || null,
|
|
133
|
+
route: params.get("route") || null,
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Sync current tag and route selection to URL
|
|
139
|
+
* Updates browser URL without reloading the page
|
|
140
|
+
*/
|
|
141
|
+
syncSelectionToUrl() {
|
|
142
|
+
if (typeof window === "undefined") {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
const params = new URLSearchParams(window.location.search)
|
|
146
|
+
if (state.leftPanel.tag) {
|
|
147
|
+
params.set("tag", state.leftPanel.tag)
|
|
148
|
+
} else {
|
|
149
|
+
params.delete("tag")
|
|
150
|
+
}
|
|
151
|
+
if (state.leftPanel.routeId) {
|
|
152
|
+
params.set("route", state.leftPanel.routeId)
|
|
153
|
+
} else {
|
|
154
|
+
params.delete("route")
|
|
155
|
+
}
|
|
156
|
+
const hash = window.location.hash || ""
|
|
157
|
+
const search = params.toString()
|
|
158
|
+
const base = window.location.pathname
|
|
159
|
+
const newUrl = search ? `${base}?${search}${hash}` : `${base}${hash}`
|
|
160
|
+
window.history.replaceState({}, "", newUrl)
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Apply selection from URL query parameters to state
|
|
165
|
+
* @param {{ tag: string|null, route: string|null }} selection
|
|
166
|
+
* @returns {boolean} - true if any selection was applied
|
|
167
|
+
*/
|
|
168
|
+
applySelectionFromQuery(selection) {
|
|
169
|
+
let applied = false
|
|
170
|
+
if (selection.tag && state.leftPanel.tags.some((tag) => tag.name === selection.tag)) {
|
|
171
|
+
state.leftPanel.tag = selection.tag
|
|
172
|
+
state.leftPanel._tag = selection.tag
|
|
173
|
+
applied = true
|
|
174
|
+
}
|
|
175
|
+
if (selection.route && state.graph.routeItems?.[selection.route]) {
|
|
176
|
+
state.leftPanel.routeId = selection.route
|
|
177
|
+
applied = true
|
|
178
|
+
const inferredTag = getters.findTagByRoute(selection.route)
|
|
179
|
+
if (inferredTag) {
|
|
180
|
+
state.leftPanel.tag = inferredTag
|
|
181
|
+
state.leftPanel._tag = inferredTag
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return applied
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Restore full tags from cache
|
|
189
|
+
* Used when resetting search mode
|
|
190
|
+
*/
|
|
191
|
+
loadFullTags() {
|
|
192
|
+
state.leftPanel.tags = state.leftPanel.fullTagsCache
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Populate field options based on selected schema
|
|
197
|
+
* @param {string} schemaId - Schema ID
|
|
198
|
+
*/
|
|
199
|
+
populateFieldOptions(schemaId) {
|
|
200
|
+
if (!schemaId) {
|
|
201
|
+
state.search.fieldOptions = []
|
|
202
|
+
state.search.fieldName = null
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
const schema = state.graph.schemaMap?.[schemaId]
|
|
206
|
+
if (!schema) {
|
|
207
|
+
state.search.fieldOptions = []
|
|
208
|
+
state.search.fieldName = null
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
const fields = Array.isArray(schema.fields) ? schema.fields.map((f) => f.name) : []
|
|
212
|
+
state.search.fieldOptions = fields
|
|
213
|
+
if (!fields.includes(state.search.fieldName)) {
|
|
214
|
+
state.search.fieldName = null
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Rebuild schema options from schema map
|
|
220
|
+
* Should be called when schema map changes
|
|
221
|
+
*/
|
|
222
|
+
rebuildSchemaOptions() {
|
|
223
|
+
const dict = state.graph.schemaMap || {}
|
|
224
|
+
const opts = Object.values(dict).map((s) => ({
|
|
225
|
+
label: s.name,
|
|
226
|
+
desc: s.id,
|
|
227
|
+
value: s.id,
|
|
228
|
+
}))
|
|
229
|
+
state.allSchemaOptions = opts
|
|
230
|
+
state.search.schemaOptions = opts.slice()
|
|
231
|
+
this.populateFieldOptions(state.search.schemaName)
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Load tags based on search criteria
|
|
236
|
+
* @returns {Promise<void>}
|
|
237
|
+
*/
|
|
238
|
+
async loadSearchedTags() {
|
|
239
|
+
try {
|
|
240
|
+
const payload = {
|
|
241
|
+
schema_name: state.search.schemaName,
|
|
242
|
+
schema_field: state.search.fieldName || null,
|
|
243
|
+
show_fields: state.filter.showFields,
|
|
244
|
+
brief: state.filter.brief,
|
|
245
|
+
hide_primitive_route: state.filter.hidePrimitiveRoute,
|
|
246
|
+
show_module: state.filter.showModule,
|
|
247
|
+
}
|
|
248
|
+
const res = await fetch("dot-search", {
|
|
249
|
+
method: "POST",
|
|
250
|
+
headers: { "Content-Type": "application/json" },
|
|
251
|
+
body: JSON.stringify(payload),
|
|
252
|
+
})
|
|
253
|
+
if (res.ok) {
|
|
254
|
+
const data = await res.json()
|
|
255
|
+
const tags = Array.isArray(data.tags) ? data.tags : []
|
|
256
|
+
state.leftPanel.tags = tags
|
|
257
|
+
}
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.error("dot-search failed", err)
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Load initial data from API
|
|
265
|
+
* @param {Function} onGenerate - Callback to generate graph after load
|
|
266
|
+
* @param {Function} renderBasedOnInitialPolicy - Callback to render based on policy
|
|
267
|
+
* @returns {Promise<void>}
|
|
268
|
+
*/
|
|
269
|
+
async loadInitial(onGenerate, renderBasedOnInitialPolicy) {
|
|
270
|
+
state.initializing = true
|
|
271
|
+
try {
|
|
272
|
+
const res = await fetch("dot")
|
|
273
|
+
const data = await res.json()
|
|
274
|
+
const tags = Array.isArray(data.tags) ? data.tags : []
|
|
275
|
+
state.leftPanel.tags = tags
|
|
276
|
+
// Cache the full tags for later use (e.g., resetSearch)
|
|
277
|
+
state.leftPanel.fullTagsCache = tags
|
|
278
|
+
|
|
279
|
+
const schemasArr = Array.isArray(data.schemas) ? data.schemas : []
|
|
280
|
+
// Build dict keyed by id for faster lookups and simpler prop passing
|
|
281
|
+
const schemaMap = Object.fromEntries(schemasArr.map((s) => [s.id, s]))
|
|
282
|
+
state.graph.schemaMap = schemaMap
|
|
283
|
+
state.graph.schemaKeys = new Set(Object.keys(schemaMap))
|
|
284
|
+
state.graph.routeItems = data.tags
|
|
285
|
+
.map((t) => t.routes)
|
|
286
|
+
.flat()
|
|
287
|
+
.reduce((acc, r) => {
|
|
288
|
+
acc[r.id] = r
|
|
289
|
+
return acc
|
|
290
|
+
}, {})
|
|
291
|
+
state.modeControl.briefModeEnabled = data.enable_brief_mode || false
|
|
292
|
+
state.version = data.version || ""
|
|
293
|
+
state.swagger.url = data.swagger_url || null
|
|
294
|
+
state.config.has_er_diagram = data.has_er_diagram || false
|
|
295
|
+
state.config.enable_pydantic_resolve_meta = data.enable_pydantic_resolve_meta || false
|
|
296
|
+
|
|
297
|
+
this.rebuildSchemaOptions()
|
|
298
|
+
|
|
299
|
+
const querySelection = this.readQuerySelection()
|
|
300
|
+
const restoredFromQuery = this.applySelectionFromQuery(querySelection)
|
|
301
|
+
if (restoredFromQuery) {
|
|
302
|
+
this.syncSelectionToUrl()
|
|
303
|
+
onGenerate()
|
|
304
|
+
return
|
|
305
|
+
} else {
|
|
306
|
+
state.config.initial_page_policy = data.initial_page_policy
|
|
307
|
+
renderBasedOnInitialPolicy()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// default route options placeholder
|
|
311
|
+
} catch (e) {
|
|
312
|
+
console.error("Initial load failed", e)
|
|
313
|
+
} finally {
|
|
314
|
+
state.initializing = false
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Filter schema options based on search text
|
|
320
|
+
* Used by Quasar select component's filter function
|
|
321
|
+
* @param {string} val - Search text
|
|
322
|
+
* @param {Function} update - Quasar update callback
|
|
323
|
+
*/
|
|
324
|
+
filterSearchSchemas(val, update) {
|
|
325
|
+
const needle = (val || "").toLowerCase()
|
|
326
|
+
update(() => {
|
|
327
|
+
if (!needle) {
|
|
328
|
+
state.search.schemaOptions = state.allSchemaOptions.slice()
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
state.search.schemaOptions = state.allSchemaOptions.filter((option) =>
|
|
332
|
+
option.label.toLowerCase().includes(needle)
|
|
333
|
+
)
|
|
334
|
+
})
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Handle schema selection change
|
|
339
|
+
* Updates state and triggers search if a schema is selected
|
|
340
|
+
* @param {string} val - Selected schema ID
|
|
341
|
+
* @param {Function} onSearch - Callback to trigger search
|
|
342
|
+
*/
|
|
343
|
+
onSearchSchemaChange(val, onSearch) {
|
|
344
|
+
state.search.schemaName = val
|
|
345
|
+
state.search.mode = false
|
|
346
|
+
if (!val) {
|
|
347
|
+
// Clearing the select should only run resetSearch via @clear
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
onSearch()
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Reset detail panels (right drawer and route detail)
|
|
355
|
+
*/
|
|
356
|
+
resetDetailPanels() {
|
|
357
|
+
state.rightDrawer.drawer = false
|
|
358
|
+
state.routeDetail.show = false
|
|
359
|
+
state.schemaDetail.schemaCodeName = ""
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Reset left panel selection and regenerate
|
|
364
|
+
* @param {Function} onGenerate - Callback to regenerate graph
|
|
365
|
+
*/
|
|
366
|
+
onReset(onGenerate) {
|
|
367
|
+
state.leftPanel.tag = null
|
|
368
|
+
state.leftPanel._tag = null
|
|
369
|
+
state.leftPanel.routeId = null
|
|
370
|
+
this.syncSelectionToUrl()
|
|
371
|
+
onGenerate()
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Toggle pydantic resolve meta visibility
|
|
376
|
+
* @param {boolean} val - New value
|
|
377
|
+
* @param {Function} onGenerate - Callback to regenerate graph
|
|
378
|
+
*/
|
|
379
|
+
togglePydanticResolveMeta(val, onGenerate) {
|
|
380
|
+
state.modeControl.pydanticResolveMetaEnabled = val
|
|
381
|
+
try {
|
|
382
|
+
localStorage.setItem("pydantic_resolve_meta", JSON.stringify(val))
|
|
383
|
+
} catch (e) {
|
|
384
|
+
console.warn("Failed to save pydantic_resolve_meta to localStorage", e)
|
|
385
|
+
}
|
|
386
|
+
onGenerate()
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Toggle show module clustering
|
|
391
|
+
* @param {boolean} val - New value
|
|
392
|
+
* @param {Function} onGenerate - Callback to regenerate graph
|
|
393
|
+
*/
|
|
394
|
+
toggleShowModule(val, onGenerate) {
|
|
395
|
+
state.filter.showModule = val
|
|
396
|
+
try {
|
|
397
|
+
localStorage.setItem("show_module_cluster", JSON.stringify(val))
|
|
398
|
+
} catch (e) {
|
|
399
|
+
console.warn("Failed to save show_module_cluster to localStorage", e)
|
|
400
|
+
}
|
|
401
|
+
onGenerate()
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Toggle show fields option
|
|
406
|
+
* @param {string} field - Field display option ("single", "object", "all")
|
|
407
|
+
* @param {Function} onGenerate - Callback to regenerate graph
|
|
408
|
+
*/
|
|
409
|
+
toggleShowField(field, onGenerate) {
|
|
410
|
+
state.filter.showFields = field
|
|
411
|
+
onGenerate(false)
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Toggle brief mode
|
|
416
|
+
* @param {boolean} val - New value
|
|
417
|
+
* @param {Function} onGenerate - Callback to regenerate graph
|
|
418
|
+
*/
|
|
419
|
+
toggleBrief(val, onGenerate) {
|
|
420
|
+
state.filter.brief = val
|
|
421
|
+
try {
|
|
422
|
+
localStorage.setItem("brief_mode", JSON.stringify(val))
|
|
423
|
+
} catch (e) {
|
|
424
|
+
console.warn("Failed to save brief_mode to localStorage", e)
|
|
425
|
+
}
|
|
426
|
+
onGenerate()
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Toggle hide primitive route
|
|
431
|
+
* @param {boolean} val - New value
|
|
432
|
+
* @param {Function} onGenerate - Callback to regenerate graph
|
|
433
|
+
*/
|
|
434
|
+
toggleHidePrimitiveRoute(val, onGenerate) {
|
|
435
|
+
state.filter.hidePrimitiveRoute = val
|
|
436
|
+
try {
|
|
437
|
+
localStorage.setItem("hide_primitive", JSON.stringify(val))
|
|
438
|
+
} catch (e) {
|
|
439
|
+
console.warn("Failed to save hide_primitive to localStorage", e)
|
|
440
|
+
}
|
|
441
|
+
onGenerate(false)
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Render based on initial page policy
|
|
446
|
+
* @param {Function} onGenerate - Callback to regenerate graph
|
|
447
|
+
*/
|
|
448
|
+
renderBasedOnInitialPolicy(onGenerate) {
|
|
449
|
+
switch (state.config.initial_page_policy) {
|
|
450
|
+
case "full":
|
|
451
|
+
onGenerate()
|
|
452
|
+
return
|
|
453
|
+
case "empty":
|
|
454
|
+
return
|
|
455
|
+
case "first":
|
|
456
|
+
state.leftPanel.tag = state.leftPanel.tags.length > 0 ? state.leftPanel.tags[0].name : null
|
|
457
|
+
state.leftPanel._tag = state.leftPanel.tag
|
|
458
|
+
this.syncSelectionToUrl()
|
|
459
|
+
onGenerate()
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Build payload for Voyager rendering
|
|
466
|
+
* @returns {Object} Payload for dot API
|
|
467
|
+
*/
|
|
468
|
+
buildVoyagerPayload() {
|
|
469
|
+
const activeSchema = state.search.mode ? state.search.schemaName : null
|
|
470
|
+
const activeField = state.search.mode ? state.search.fieldName : null
|
|
471
|
+
return {
|
|
472
|
+
tags: state.leftPanel.tag ? [state.leftPanel.tag] : null,
|
|
473
|
+
schema_name: activeSchema || null,
|
|
474
|
+
schema_field: activeField || null,
|
|
475
|
+
route_name: state.leftPanel.routeId || null,
|
|
476
|
+
show_fields: state.filter.showFields,
|
|
477
|
+
brief: state.filter.brief,
|
|
478
|
+
hide_primitive_route: state.filter.hidePrimitiveRoute,
|
|
479
|
+
show_module: state.filter.showModule,
|
|
480
|
+
show_pydantic_resolve_meta: state.modeControl.pydanticResolveMetaEnabled,
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Build payload for ER Diagram rendering
|
|
486
|
+
* @returns {Object} Payload for er-diagram API
|
|
487
|
+
*/
|
|
488
|
+
buildErDiagramPayload() {
|
|
489
|
+
return {
|
|
490
|
+
show_fields: state.filter.showFields,
|
|
491
|
+
show_module: state.filter.showModule,
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Restore search state and return whether to regenerate
|
|
497
|
+
* @returns {boolean} - true if should regenerate with previous selection
|
|
498
|
+
*/
|
|
499
|
+
resetSearchState() {
|
|
500
|
+
state.search.mode = false
|
|
501
|
+
const hadPreviousValue = state.previousTagRoute.hasValue
|
|
502
|
+
|
|
503
|
+
if (hadPreviousValue) {
|
|
504
|
+
state.leftPanel.tag = state.previousTagRoute.tag
|
|
505
|
+
state.leftPanel._tag = state.previousTagRoute.tag
|
|
506
|
+
state.leftPanel.routeId = state.previousTagRoute.routeId
|
|
507
|
+
// Clear the saved state
|
|
508
|
+
state.previousTagRoute.hasValue = false
|
|
509
|
+
} else {
|
|
510
|
+
state.leftPanel.tag = null
|
|
511
|
+
state.leftPanel._tag = null
|
|
512
|
+
state.leftPanel.routeId = null
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
this.syncSelectionToUrl()
|
|
516
|
+
this.loadFullTags()
|
|
517
|
+
|
|
518
|
+
return hadPreviousValue
|
|
519
|
+
},
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const mutations = {}
|
|
523
|
+
|
|
524
|
+
export const store = {
|
|
525
|
+
state,
|
|
526
|
+
getters,
|
|
527
|
+
actions,
|
|
528
|
+
mutations,
|
|
529
|
+
}
|