fastapi-voyager 0.15.5__py3-none-any.whl → 0.16.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_voyager/__init__.py +2 -2
- fastapi_voyager/adapters/__init__.py +16 -0
- fastapi_voyager/adapters/base.py +44 -0
- fastapi_voyager/adapters/common.py +260 -0
- fastapi_voyager/adapters/django_ninja_adapter.py +299 -0
- fastapi_voyager/adapters/fastapi_adapter.py +165 -0
- fastapi_voyager/adapters/litestar_adapter.py +188 -0
- fastapi_voyager/er_diagram.py +15 -14
- fastapi_voyager/introspectors/__init__.py +34 -0
- fastapi_voyager/introspectors/base.py +81 -0
- fastapi_voyager/introspectors/detector.py +123 -0
- fastapi_voyager/introspectors/django_ninja.py +114 -0
- fastapi_voyager/introspectors/fastapi.py +83 -0
- fastapi_voyager/introspectors/litestar.py +166 -0
- fastapi_voyager/pydantic_resolve_util.py +4 -2
- fastapi_voyager/render.py +2 -2
- fastapi_voyager/render_style.py +0 -1
- fastapi_voyager/server.py +174 -295
- fastapi_voyager/type_helper.py +2 -2
- fastapi_voyager/version.py +1 -1
- fastapi_voyager/voyager.py +75 -47
- fastapi_voyager/web/graph-ui.js +102 -69
- fastapi_voyager/web/graphviz.svg.js +79 -30
- fastapi_voyager/web/index.html +11 -14
- fastapi_voyager/web/store.js +2 -0
- fastapi_voyager/web/vue-main.js +4 -0
- {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/METADATA +133 -7
- {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/RECORD +31 -19
- {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/WHEEL +0 -0
- {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/entry_points.txt +0 -0
- {fastapi_voyager-0.15.5.dist-info → fastapi_voyager-0.16.0a1.dist-info}/licenses/LICENSE +0 -0
fastapi_voyager/voyager.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
|
|
2
2
|
import pydantic_resolve.constant as const
|
|
3
|
-
from fastapi import FastAPI, routing
|
|
4
3
|
from pydantic import BaseModel
|
|
5
4
|
|
|
6
5
|
from fastapi_voyager.filter import (
|
|
@@ -8,6 +7,7 @@ from fastapi_voyager.filter import (
|
|
|
8
7
|
filter_subgraph_by_module_prefix,
|
|
9
8
|
filter_subgraph_from_tag_to_schema_by_module_prefix,
|
|
10
9
|
)
|
|
10
|
+
from fastapi_voyager.introspectors import AppIntrospector, RouteInfo
|
|
11
11
|
from fastapi_voyager.render import Renderer
|
|
12
12
|
from fastapi_voyager.type import PK, CoreData, FieldType, Link, LinkType, Route, SchemaNode, Tag
|
|
13
13
|
from fastapi_voyager.type_helper import (
|
|
@@ -57,99 +57,127 @@ class Voyager:
|
|
|
57
57
|
self.hide_primitive_route = hide_primitive_route
|
|
58
58
|
self.show_module = show_module
|
|
59
59
|
self.show_pydantic_resolve_meta = show_pydantic_resolve_meta
|
|
60
|
-
|
|
61
60
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
def _get_introspector(self, app) -> AppIntrospector:
|
|
62
|
+
"""
|
|
63
|
+
Get the appropriate introspector for the given app.
|
|
64
|
+
|
|
65
|
+
Automatically detects the framework type and returns the matching introspector.
|
|
66
66
|
|
|
67
|
+
Args:
|
|
68
|
+
app: A web application instance or AppIntrospector
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
Returns:
|
|
71
|
+
An AppIntrospector instance
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
TypeError: If the app type is not supported
|
|
69
75
|
"""
|
|
76
|
+
from fastapi_voyager.introspectors import get_introspector
|
|
77
|
+
|
|
78
|
+
return get_introspector(app)
|
|
79
|
+
|
|
80
|
+
def analysis(self, app):
|
|
81
|
+
"""
|
|
82
|
+
Analyze routes and schemas from a web application.
|
|
83
|
+
|
|
84
|
+
This method automatically detects the framework type and uses the appropriate
|
|
85
|
+
introspector. Supported frameworks:
|
|
86
|
+
- FastAPI (built-in)
|
|
87
|
+
- Any framework with a custom AppIntrospector implementation
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
app: A web application instance (FastAPI, Django Ninja API, etc.)
|
|
91
|
+
or an AppIntrospector instance for custom frameworks.
|
|
92
|
+
|
|
70
93
|
1. get routes which return pydantic schema
|
|
71
94
|
1.1 collect tags and routes, add links tag-> route
|
|
72
95
|
1.2 collect response_model and links route -> response_model
|
|
73
96
|
|
|
74
97
|
2. iterate schemas, construct the schema/model nodes and their links
|
|
75
98
|
"""
|
|
99
|
+
introspector = self._get_introspector(app)
|
|
76
100
|
schemas: list[type[BaseModel]] = []
|
|
77
101
|
|
|
78
102
|
# First, group all routes by tag
|
|
79
|
-
routes_by_tag: dict[str, list] = {}
|
|
80
|
-
for
|
|
81
|
-
tags = getattr(route, 'tags', None)
|
|
82
|
-
|
|
103
|
+
routes_by_tag: dict[str, list[RouteInfo]] = {}
|
|
104
|
+
for route_info in introspector.get_routes():
|
|
83
105
|
# using multiple tags is harmful, it's not recommended and will not be supported
|
|
84
|
-
route_tag = tags[0] if tags else '__default__'
|
|
85
|
-
routes_by_tag.setdefault(route_tag, []).append(
|
|
106
|
+
route_tag = route_info.tags[0] if route_info.tags else '__default__'
|
|
107
|
+
routes_by_tag.setdefault(route_tag, []).append(route_info)
|
|
86
108
|
|
|
87
109
|
# Then filter by include_tags if provided
|
|
88
110
|
if self.include_tags:
|
|
89
|
-
filtered_routes_by_tag = {
|
|
90
|
-
|
|
111
|
+
filtered_routes_by_tag = {
|
|
112
|
+
tag: routes
|
|
113
|
+
for tag, routes in routes_by_tag.items()
|
|
114
|
+
if tag in self.include_tags
|
|
115
|
+
}
|
|
91
116
|
else:
|
|
92
117
|
filtered_routes_by_tag = routes_by_tag
|
|
93
118
|
|
|
94
119
|
# Process filtered routes
|
|
95
|
-
for route_tag,
|
|
96
|
-
|
|
120
|
+
for route_tag, route_infos in filtered_routes_by_tag.items():
|
|
97
121
|
tag_id = f'tag__{route_tag}'
|
|
98
122
|
tag_obj = Tag(id=tag_id, name=route_tag, routes=[])
|
|
99
123
|
self.tags.append(tag_obj)
|
|
100
124
|
|
|
101
|
-
for
|
|
102
|
-
# add route and create links
|
|
103
|
-
route_id = full_class_name(route.endpoint)
|
|
104
|
-
route_name = route.endpoint.__name__
|
|
105
|
-
route_module = route.endpoint.__module__
|
|
106
|
-
|
|
125
|
+
for route_info in route_infos:
|
|
107
126
|
# filter by route_name (route.id) if provided
|
|
108
|
-
if self.route_name is not None and
|
|
127
|
+
if self.route_name is not None and route_info.id != self.route_name:
|
|
109
128
|
continue
|
|
110
129
|
|
|
111
|
-
is_primitive_response = is_non_pydantic_type(
|
|
130
|
+
is_primitive_response = is_non_pydantic_type(route_info.response_model)
|
|
112
131
|
# filter primitive route if needed
|
|
113
132
|
if self.hide_primitive_route and is_primitive_response:
|
|
114
133
|
continue
|
|
115
134
|
|
|
116
|
-
self.links.append(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
135
|
+
self.links.append(
|
|
136
|
+
Link(
|
|
137
|
+
source=tag_id,
|
|
138
|
+
source_origin=tag_id,
|
|
139
|
+
target=route_info.id,
|
|
140
|
+
target_origin=route_info.id,
|
|
141
|
+
type='tag_route',
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Get unique_id from extra data if available
|
|
146
|
+
unique_id = route_info.operation_id
|
|
147
|
+
if route_info.extra and 'unique_id' in route_info.extra:
|
|
148
|
+
unique_id = unique_id or route_info.extra['unique_id']
|
|
123
149
|
|
|
124
150
|
route_obj = Route(
|
|
125
|
-
id=
|
|
126
|
-
name=
|
|
127
|
-
module=
|
|
128
|
-
unique_id=
|
|
129
|
-
response_schema=get_type_name(
|
|
130
|
-
is_primitive=is_primitive_response
|
|
151
|
+
id=route_info.id,
|
|
152
|
+
name=route_info.name,
|
|
153
|
+
module=route_info.module,
|
|
154
|
+
unique_id=unique_id,
|
|
155
|
+
response_schema=get_type_name(route_info.response_model),
|
|
156
|
+
is_primitive=is_primitive_response,
|
|
131
157
|
)
|
|
132
158
|
self.routes.append(route_obj)
|
|
133
159
|
tag_obj.routes.append(route_obj)
|
|
134
160
|
|
|
135
161
|
# add response_models and create links from route -> response_model
|
|
136
|
-
for schema in get_core_types(
|
|
162
|
+
for schema in get_core_types(route_info.response_model):
|
|
137
163
|
if schema and issubclass(schema, BaseModel):
|
|
138
164
|
is_primitive_response = False
|
|
139
165
|
target_name = full_class_name(schema)
|
|
140
|
-
self.links.append(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
166
|
+
self.links.append(
|
|
167
|
+
Link(
|
|
168
|
+
source=route_info.id,
|
|
169
|
+
source_origin=route_info.id,
|
|
170
|
+
target=self.generate_node_head(target_name),
|
|
171
|
+
target_origin=target_name,
|
|
172
|
+
type='route_to_schema',
|
|
173
|
+
)
|
|
174
|
+
)
|
|
147
175
|
|
|
148
176
|
schemas.append(schema)
|
|
149
177
|
|
|
150
178
|
for s in schemas:
|
|
151
179
|
self.analysis_schemas(s)
|
|
152
|
-
|
|
180
|
+
|
|
153
181
|
self.nodes = list(self.node_set.values())
|
|
154
182
|
|
|
155
183
|
|
fastapi_voyager/web/graph-ui.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
export class GraphUI {
|
|
2
|
+
// ====================
|
|
3
|
+
// Constants
|
|
4
|
+
// ====================
|
|
5
|
+
|
|
6
|
+
static HIGHLIGHT_COLOR = "#822dba"
|
|
7
|
+
static HIGHLIGHT_STROKE_WIDTH = "3.0"
|
|
8
|
+
|
|
9
|
+
// ====================
|
|
10
|
+
// Constructor
|
|
11
|
+
// ====================
|
|
12
|
+
|
|
2
13
|
constructor(selector = "#graph", options = {}) {
|
|
3
14
|
this.selector = selector
|
|
4
15
|
this.options = options // e.g. { onSchemaClick: (name) => {} }
|
|
@@ -9,6 +20,10 @@ export class GraphUI {
|
|
|
9
20
|
this._init()
|
|
10
21
|
}
|
|
11
22
|
|
|
23
|
+
// ====================
|
|
24
|
+
// Highlight Methods
|
|
25
|
+
// ====================
|
|
26
|
+
|
|
12
27
|
_highlight(mode = "bidirectional") {
|
|
13
28
|
let highlightedNodes = $()
|
|
14
29
|
for (const selection of this.currentSelection) {
|
|
@@ -68,49 +83,79 @@ export class GraphUI {
|
|
|
68
83
|
return $result
|
|
69
84
|
}
|
|
70
85
|
|
|
86
|
+
// ====================
|
|
87
|
+
// Schema Banner Methods
|
|
88
|
+
// ====================
|
|
89
|
+
|
|
71
90
|
highlightSchemaBanner(node) {
|
|
72
|
-
// Get all polygons in the node
|
|
73
91
|
const polygons = node.querySelectorAll("polygon")
|
|
74
|
-
|
|
75
|
-
// The first polygon is typically the outer frame of the entire node
|
|
76
92
|
const outerFrame = polygons[0]
|
|
77
|
-
// The second polygon is typically the title background
|
|
78
93
|
const titleBg = polygons[1]
|
|
79
94
|
|
|
80
95
|
if (outerFrame) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
outerFrame.setAttribute(
|
|
85
|
-
"data-original-stroke-width",
|
|
86
|
-
outerFrame.getAttribute("stroke-width") || "1"
|
|
87
|
-
)
|
|
88
|
-
outerFrame.setAttribute("data-original-fill", outerFrame.getAttribute("fill") || "")
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Apply bold purple border to the outer frame
|
|
92
|
-
outerFrame.setAttribute("stroke", "#822dba")
|
|
93
|
-
outerFrame.setAttribute("stroke-width", "3.0")
|
|
96
|
+
this._saveOriginalAttributes(outerFrame)
|
|
97
|
+
outerFrame.setAttribute("stroke", GraphUI.HIGHLIGHT_COLOR)
|
|
98
|
+
outerFrame.setAttribute("stroke-width", GraphUI.HIGHLIGHT_STROKE_WIDTH)
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
if (titleBg) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
titleBg.getAttribute("stroke-width") || "1"
|
|
103
|
-
)
|
|
104
|
-
titleBg.setAttribute("data-original-fill", titleBg.getAttribute("fill") || "")
|
|
105
|
-
}
|
|
102
|
+
this._saveOriginalAttributes(titleBg)
|
|
103
|
+
titleBg.setAttribute("fill", GraphUI.HIGHLIGHT_COLOR)
|
|
104
|
+
titleBg.setAttribute("stroke", GraphUI.HIGHLIGHT_COLOR)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
clearSchemaBanners() {
|
|
109
|
+
if (this.gv) {
|
|
110
|
+
this.gv.highlight()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const allPolygons = document.querySelectorAll("polygon[data-original-stroke]")
|
|
114
|
+
allPolygons.forEach((polygon) => {
|
|
115
|
+
polygon.removeAttribute("data-original-stroke")
|
|
116
|
+
polygon.removeAttribute("data-original-stroke-width")
|
|
117
|
+
polygon.removeAttribute("data-original-fill")
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_saveOriginalAttributes(element) {
|
|
122
|
+
if (!element.hasAttribute("data-original-stroke")) {
|
|
123
|
+
element.setAttribute("data-original-stroke", element.getAttribute("stroke") || "")
|
|
124
|
+
element.setAttribute(
|
|
125
|
+
"data-original-stroke-width",
|
|
126
|
+
element.getAttribute("stroke-width") || "1"
|
|
127
|
+
)
|
|
128
|
+
element.setAttribute("data-original-fill", element.getAttribute("fill") || "")
|
|
111
129
|
}
|
|
112
130
|
}
|
|
113
131
|
|
|
132
|
+
_applyNodeHighlight(node) {
|
|
133
|
+
const set = $()
|
|
134
|
+
set.push(node)
|
|
135
|
+
const obj = { set, direction: "bidirectional" }
|
|
136
|
+
|
|
137
|
+
this.clearSchemaBanners()
|
|
138
|
+
this.currentSelection = [obj]
|
|
139
|
+
this._highlight()
|
|
140
|
+
|
|
141
|
+
return obj
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_triggerCallback(callbackName, schemaName) {
|
|
145
|
+
const callback = this.options[callbackName]
|
|
146
|
+
if (callback && schemaName) {
|
|
147
|
+
try {
|
|
148
|
+
callback(schemaName)
|
|
149
|
+
} catch (e) {
|
|
150
|
+
console.warn(`${callbackName} callback failed`, e)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ====================
|
|
156
|
+
// Initialization & Events
|
|
157
|
+
// ====================
|
|
158
|
+
|
|
114
159
|
_init() {
|
|
115
160
|
const self = this
|
|
116
161
|
$(this.selector).graphviz({
|
|
@@ -127,80 +172,64 @@ export class GraphUI {
|
|
|
127
172
|
|
|
128
173
|
nodes.on("dblclick.graphui", function (event) {
|
|
129
174
|
event.stopPropagation()
|
|
175
|
+
|
|
176
|
+
self._applyNodeHighlight(this)
|
|
177
|
+
|
|
130
178
|
try {
|
|
131
179
|
self.highlightSchemaBanner(this)
|
|
132
180
|
} catch (e) {
|
|
133
181
|
console.log(e)
|
|
134
182
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const schemaName = event.currentTarget.dataset.name
|
|
138
|
-
if (schemaName) {
|
|
139
|
-
try {
|
|
140
|
-
self.options.onSchemaClick(schemaName)
|
|
141
|
-
} catch (e) {
|
|
142
|
-
console.warn("onSchemaClick callback failed", e)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
183
|
+
|
|
184
|
+
self._triggerCallback("onSchemaClick", event.currentTarget.dataset.name)
|
|
145
185
|
})
|
|
146
186
|
|
|
147
187
|
edges.on("click.graphui", function (event) {
|
|
188
|
+
const [upStreamNode, downStreamNode] = event.currentTarget.dataset.name.split("->")
|
|
189
|
+
const nodes = self.gv.nodesByName()
|
|
190
|
+
|
|
148
191
|
const up = $()
|
|
149
192
|
const down = $()
|
|
150
193
|
const edge = $()
|
|
151
|
-
|
|
152
|
-
const nodes = self.gv.nodesByName()
|
|
194
|
+
|
|
153
195
|
up.push(nodes[upStreamNode])
|
|
154
196
|
down.push(nodes[downStreamNode])
|
|
155
197
|
edge.push(this)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
198
|
+
|
|
199
|
+
self.currentSelection = [
|
|
200
|
+
{ set: up, direction: "upstream" },
|
|
201
|
+
{ set: down, direction: "downstream" },
|
|
202
|
+
{ set: edge, direction: "single" },
|
|
203
|
+
]
|
|
160
204
|
|
|
161
205
|
self._highlightEdgeNodes()
|
|
162
206
|
})
|
|
163
207
|
|
|
164
208
|
nodes.on("click.graphui", function (event) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const obj = { set, direction: "bidirectional" }
|
|
168
|
-
|
|
169
|
-
const schemaName = event.currentTarget.dataset.name
|
|
170
|
-
console.log("shift click detected")
|
|
171
|
-
if (event.shiftKey && self.options.onSchemaShiftClick) {
|
|
172
|
-
if (schemaName) {
|
|
173
|
-
try {
|
|
174
|
-
self.options.onSchemaShiftClick(schemaName)
|
|
175
|
-
} catch (e) {
|
|
176
|
-
console.warn("onSchemaShiftClick callback failed", e)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
209
|
+
if (event.shiftKey) {
|
|
210
|
+
self._triggerCallback("onSchemaShiftClick", event.currentTarget.dataset.name)
|
|
179
211
|
} else {
|
|
180
|
-
self.
|
|
181
|
-
self._highlight()
|
|
212
|
+
self._applyNodeHighlight(this)
|
|
182
213
|
}
|
|
183
214
|
})
|
|
184
215
|
|
|
185
216
|
$(document)
|
|
186
217
|
.off("click.graphui")
|
|
187
218
|
.on("click.graphui", function (evt) {
|
|
188
|
-
// if outside container, do nothing
|
|
189
219
|
const graphContainer = $(self.selector)[0]
|
|
190
220
|
if (!graphContainer || !evt.target || !graphContainer.contains(evt.target)) {
|
|
191
221
|
return
|
|
192
222
|
}
|
|
193
223
|
|
|
194
|
-
let isNode = false
|
|
195
224
|
const $everything = self.gv.$nodes.add(self.gv.$edges).add(self.gv.$clusters)
|
|
196
225
|
const node = evt.target.parentNode
|
|
197
|
-
$everything.
|
|
198
|
-
|
|
199
|
-
isNode = true
|
|
200
|
-
}
|
|
226
|
+
const isNode = $everything.is(function () {
|
|
227
|
+
return this === node
|
|
201
228
|
})
|
|
229
|
+
|
|
202
230
|
if (!isNode && self.gv) {
|
|
203
|
-
self.
|
|
231
|
+
self.clearSchemaBanners()
|
|
232
|
+
|
|
204
233
|
if (self.options.resetCb) {
|
|
205
234
|
self.options.resetCb()
|
|
206
235
|
}
|
|
@@ -210,6 +239,10 @@ export class GraphUI {
|
|
|
210
239
|
})
|
|
211
240
|
}
|
|
212
241
|
|
|
242
|
+
// ====================
|
|
243
|
+
// Render Method
|
|
244
|
+
// ====================
|
|
245
|
+
|
|
213
246
|
async render(dotSrc, resetZoom = true) {
|
|
214
247
|
const height = this.options.height || "100%"
|
|
215
248
|
return new Promise((resolve, reject) => {
|
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
|
|
18
18
|
GraphvizSvg.GVPT_2_PX = 32.5 // used to ease removal of extra space
|
|
19
19
|
|
|
20
|
+
// SVG element selectors for color manipulation
|
|
21
|
+
// NOTE: If you need to add more element types for highlighting/dimming,
|
|
22
|
+
// update SHAPE_ELEMENTS and the code will automatically handle them
|
|
23
|
+
GraphvizSvg.SHAPE_ELEMENTS = "polygon, ellipse, path, polyline"
|
|
24
|
+
GraphvizSvg.TEXT_ELEMENTS = "text"
|
|
25
|
+
GraphvizSvg.ALL_COLOR_ELEMENTS = GraphvizSvg.SHAPE_ELEMENTS + ", " + GraphvizSvg.TEXT_ELEMENTS
|
|
26
|
+
|
|
20
27
|
GraphvizSvg.DEFAULTS = {
|
|
21
28
|
url: null,
|
|
22
29
|
svg: null,
|
|
@@ -178,8 +185,8 @@
|
|
|
178
185
|
this.setInteractiveCursor($el, type === "edge")
|
|
179
186
|
}
|
|
180
187
|
|
|
181
|
-
//
|
|
182
|
-
$el.find(
|
|
188
|
+
// Save the colors of shape elements (polygon, ellipse, path, polyline)
|
|
189
|
+
$el.find(GraphvizSvg.SHAPE_ELEMENTS).each(function () {
|
|
183
190
|
var $this = $(this)
|
|
184
191
|
if ($this.attr("data-graphviz-hitbox") === "true") {
|
|
185
192
|
return
|
|
@@ -196,6 +203,20 @@
|
|
|
196
203
|
}
|
|
197
204
|
})
|
|
198
205
|
|
|
206
|
+
// Save the colors of text elements
|
|
207
|
+
$el.find(GraphvizSvg.TEXT_ELEMENTS).each(function () {
|
|
208
|
+
var $this = $(this)
|
|
209
|
+
// text elements might not have explicit fill attribute, use black as default
|
|
210
|
+
var fill = $this.attr("fill")
|
|
211
|
+
if (!fill || fill === "none") {
|
|
212
|
+
fill = "#000000" // default black color for text
|
|
213
|
+
}
|
|
214
|
+
$this.data("graphviz.svg.color", {
|
|
215
|
+
fill: fill,
|
|
216
|
+
stroke: $this.attr("stroke"),
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
199
220
|
// save the node name and check if theres a comment above; save it
|
|
200
221
|
var $title = $el.children("title")
|
|
201
222
|
if ($title[0]) {
|
|
@@ -404,6 +425,57 @@
|
|
|
404
425
|
return retval
|
|
405
426
|
}
|
|
406
427
|
|
|
428
|
+
// Helper function to apply color transformation to elements
|
|
429
|
+
GraphvizSvg.prototype._applyColorToElements = function (
|
|
430
|
+
$elements,
|
|
431
|
+
colorTransformer,
|
|
432
|
+
bgColor,
|
|
433
|
+
setStrokeWidth
|
|
434
|
+
) {
|
|
435
|
+
var that = this
|
|
436
|
+
$elements.each(function () {
|
|
437
|
+
var $this = $(this)
|
|
438
|
+
if ($this.attr("data-graphviz-hitbox") === "true") {
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
var color = $this.data("graphviz.svg.color")
|
|
442
|
+
if (color) {
|
|
443
|
+
if (color.fill && color.fill != "none") {
|
|
444
|
+
$this.attr("fill", colorTransformer(color.fill, bgColor))
|
|
445
|
+
}
|
|
446
|
+
if (color.stroke && color.stroke != "none") {
|
|
447
|
+
$this.attr("stroke", colorTransformer(color.stroke, bgColor))
|
|
448
|
+
}
|
|
449
|
+
if (setStrokeWidth !== undefined) {
|
|
450
|
+
$this.attr("stroke-width", setStrokeWidth)
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Helper function to restore original colors
|
|
457
|
+
GraphvizSvg.prototype._restoreElementColors = function ($elements, setStrokeWidth) {
|
|
458
|
+
var that = this
|
|
459
|
+
$elements.each(function () {
|
|
460
|
+
var $this = $(this)
|
|
461
|
+
if ($this.attr("data-graphviz-hitbox") === "true") {
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
var color = $this.data("graphviz.svg.color")
|
|
465
|
+
if (color) {
|
|
466
|
+
if (color.fill && color.fill != "none") {
|
|
467
|
+
$this.attr("fill", color.fill)
|
|
468
|
+
}
|
|
469
|
+
if (color.stroke && color.stroke != "none") {
|
|
470
|
+
$this.attr("stroke", color.stroke)
|
|
471
|
+
}
|
|
472
|
+
if (setStrokeWidth !== undefined) {
|
|
473
|
+
$this.attr("stroke-width", setStrokeWidth)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
|
|
407
479
|
GraphvizSvg.prototype.findEdge = function (nodeName, testEdge, $retval) {
|
|
408
480
|
var retval = []
|
|
409
481
|
for (var name in this._edgesByName) {
|
|
@@ -439,37 +511,14 @@
|
|
|
439
511
|
|
|
440
512
|
GraphvizSvg.prototype.colorElement = function ($el, getColor) {
|
|
441
513
|
var bg = this.$element.css("background")
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
return
|
|
446
|
-
}
|
|
447
|
-
var color = $this.data("graphviz.svg.color")
|
|
448
|
-
if (color.fill && color.fill != "none") {
|
|
449
|
-
$this.attr("fill", getColor(color.fill, bg)) // don't set fill if it's a path
|
|
450
|
-
}
|
|
451
|
-
if (color.stroke && color.stroke != "none") {
|
|
452
|
-
$this.attr("stroke", getColor(color.stroke, bg))
|
|
453
|
-
}
|
|
454
|
-
$this.attr("stroke-width", 1.6)
|
|
455
|
-
})
|
|
514
|
+
|
|
515
|
+
// Apply color transformation to all elements (shapes + text)
|
|
516
|
+
this._applyColorToElements($el.find(GraphvizSvg.ALL_COLOR_ELEMENTS), getColor, bg)
|
|
456
517
|
}
|
|
457
518
|
|
|
458
519
|
GraphvizSvg.prototype.restoreElement = function ($el) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if ($this.attr("data-graphviz-hitbox") === "true") {
|
|
462
|
-
return
|
|
463
|
-
}
|
|
464
|
-
var color = $this.data("graphviz.svg.color")
|
|
465
|
-
if (color.fill && color.fill != "none") {
|
|
466
|
-
$this.attr("fill", color.fill) // don't set fill if it's a path
|
|
467
|
-
}
|
|
468
|
-
if (color.stroke && color.stroke != "none") {
|
|
469
|
-
$this.attr("stroke", color.stroke)
|
|
470
|
-
}
|
|
471
|
-
$this.attr("stroke-width", 1)
|
|
472
|
-
})
|
|
520
|
+
// Restore original colors for all elements (shapes + text)
|
|
521
|
+
this._restoreElementColors($el.find(GraphvizSvg.ALL_COLOR_ELEMENTS), 1)
|
|
473
522
|
}
|
|
474
523
|
|
|
475
524
|
// methods users can actually call
|
fastapi_voyager/web/index.html
CHANGED
|
@@ -4,32 +4,29 @@
|
|
|
4
4
|
<meta name="theme-color" content="#ffffff" />
|
|
5
5
|
<link
|
|
6
6
|
rel="stylesheet"
|
|
7
|
-
href="
|
|
8
|
-
/>
|
|
9
|
-
<link
|
|
10
|
-
rel="stylesheet"
|
|
11
|
-
href="fastapi-voyager-static/quasar.min.css<!-- VERSION_PLACEHOLDER -->"
|
|
7
|
+
href="<!-- STATIC_PATH -->/graphviz.svg.css<!-- VERSION_PLACEHOLDER -->"
|
|
12
8
|
/>
|
|
9
|
+
<link rel="stylesheet" href="<!-- STATIC_PATH -->/quasar.min.css<!-- VERSION_PLACEHOLDER -->" />
|
|
13
10
|
<!-- App Icons / Favicons -->
|
|
14
11
|
<link
|
|
15
12
|
rel="apple-touch-icon"
|
|
16
13
|
sizes="180x180"
|
|
17
|
-
href="
|
|
14
|
+
href="<!-- STATIC_PATH -->/icon/apple-touch-icon.png"
|
|
18
15
|
/>
|
|
19
16
|
<link
|
|
20
17
|
rel="icon"
|
|
21
18
|
type="image/png"
|
|
22
19
|
sizes="32x32"
|
|
23
|
-
href="
|
|
20
|
+
href="<!-- STATIC_PATH -->/icon/favicon-32x32.png"
|
|
24
21
|
/>
|
|
25
22
|
<link
|
|
26
23
|
rel="icon"
|
|
27
24
|
type="image/png"
|
|
28
25
|
sizes="16x16"
|
|
29
|
-
href="
|
|
26
|
+
href="<!-- STATIC_PATH -->/icon/favicon-16x16.png"
|
|
30
27
|
/>
|
|
31
|
-
<link rel="icon" href="
|
|
32
|
-
<link rel="manifest" href="
|
|
28
|
+
<link rel="icon" href="<!-- STATIC_PATH -->/icon/favicon.ico" sizes="any" />
|
|
29
|
+
<link rel="manifest" href="<!-- STATIC_PATH -->/icon/site.webmanifest" />
|
|
33
30
|
<link
|
|
34
31
|
href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
|
|
35
32
|
rel="stylesheet"
|
|
@@ -191,7 +188,7 @@
|
|
|
191
188
|
style="font-size: 18px; font-weight: bold; display: flex; align-items: baseline"
|
|
192
189
|
>
|
|
193
190
|
<q-icon class="q-mr-sm" name="satellite_alt"></q-icon>
|
|
194
|
-
<span>
|
|
191
|
+
<span> {{ store.state.framework_name }} Voyager </span>
|
|
195
192
|
<span
|
|
196
193
|
v-if="store.state.version"
|
|
197
194
|
style="font-size: 12px; margin-left: 8px; font-weight: normal"
|
|
@@ -580,7 +577,7 @@
|
|
|
580
577
|
</q-dialog>
|
|
581
578
|
</div>
|
|
582
579
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
583
|
-
<script src="
|
|
580
|
+
<script src="<!-- STATIC_PATH -->/quasar.min.js<!-- VERSION_PLACEHOLDER -->"></script>
|
|
584
581
|
<script
|
|
585
582
|
src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"
|
|
586
583
|
integrity="sha512-egJ/Y+22P9NQ9aIyVCh0VCOsfydyn8eNmqBy+y2CnJG+fpRIxXMS6jbWP8tVKp0jp+NO5n8WtMUAnNnGoJKi4w=="
|
|
@@ -602,7 +599,7 @@
|
|
|
602
599
|
></script>
|
|
603
600
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.1.13/jquery.mousewheel.min.js"></script>
|
|
604
601
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-color/2.1.2/jquery.color.min.js"></script>
|
|
605
|
-
<script src="
|
|
602
|
+
<script src="<!-- STATIC_PATH -->/graphviz.svg.js<!-- VERSION_PLACEHOLDER -->"></script>
|
|
606
603
|
<!-- highlight.js minimal ES module load (python only) -->
|
|
607
604
|
<link
|
|
608
605
|
rel="stylesheet"
|
|
@@ -626,7 +623,7 @@
|
|
|
626
623
|
</script>
|
|
627
624
|
<script
|
|
628
625
|
type="module"
|
|
629
|
-
src="
|
|
626
|
+
src="<!-- STATIC_PATH -->/vue-main.js<!-- VERSION_PLACEHOLDER -->"
|
|
630
627
|
></script>
|
|
631
628
|
|
|
632
629
|
<!-- GA_SNIPPET -->
|
fastapi_voyager/web/store.js
CHANGED
|
@@ -2,6 +2,7 @@ const { reactive } = window.Vue
|
|
|
2
2
|
|
|
3
3
|
const state = reactive({
|
|
4
4
|
version: "",
|
|
5
|
+
framework_name: "",
|
|
5
6
|
config: {
|
|
6
7
|
initial_page_policy: "first",
|
|
7
8
|
has_er_diagram: false,
|
|
@@ -305,6 +306,7 @@ const actions = {
|
|
|
305
306
|
state.swagger.url = data.swagger_url || null
|
|
306
307
|
state.config.has_er_diagram = data.has_er_diagram || false
|
|
307
308
|
state.config.enable_pydantic_resolve_meta = data.enable_pydantic_resolve_meta || false
|
|
309
|
+
state.framework_name = data.framework_name || "API"
|
|
308
310
|
|
|
309
311
|
this.rebuildSchemaOptions()
|
|
310
312
|
|