fastapi-voyager 0.4.1__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.
@@ -0,0 +1,232 @@
1
+ import inspect
2
+ import os
3
+ from pydantic import BaseModel
4
+ from typing import get_origin, get_args, Union, Annotated, Any
5
+ from fastapi_voyager.type import FieldInfo
6
+ from types import UnionType
7
+
8
+ # Python <3.12 compatibility: TypeAliasType exists only from 3.12 (PEP 695)
9
+ try: # pragma: no cover - import guard
10
+ from typing import TypeAliasType # type: ignore
11
+ except Exception: # pragma: no cover
12
+ class _DummyTypeAliasType: # minimal sentinel so isinstance checks are safe
13
+ pass
14
+ TypeAliasType = _DummyTypeAliasType # type: ignore
15
+
16
+
17
+ def _is_optional(annotation):
18
+ origin = get_origin(annotation)
19
+ args = get_args(annotation)
20
+ if origin is Union and type(None) in args:
21
+ return True
22
+ return False
23
+
24
+
25
+ def _is_list(annotation):
26
+ return getattr(annotation, "__origin__", None) == list
27
+
28
+
29
+ def shelling_type(type):
30
+ while _is_optional(type) or _is_list(type):
31
+ type = type.__args__[0]
32
+ return type
33
+
34
+
35
+ def full_class_name(cls):
36
+ return f"{cls.__module__}.{cls.__qualname__}"
37
+
38
+
39
+ def get_core_types(tp):
40
+ """
41
+ - get the core type
42
+ - always return a tuple of core types
43
+ """
44
+ if tp is type(None):
45
+ return tuple()
46
+
47
+ # Unwrap PEP 695 type aliases (they wrap the actual annotation in __value__)
48
+ # Repeat in case of nested aliasing.
49
+ def _unwrap_alias(t):
50
+ while isinstance(t, TypeAliasType) or (
51
+ t.__class__.__name__ == 'TypeAliasType' and hasattr(t, '__value__')
52
+ ):
53
+ try:
54
+ t = t.__value__
55
+ except Exception: # pragma: no cover - defensive
56
+ break
57
+ return t
58
+
59
+ tp = _unwrap_alias(tp)
60
+
61
+ # 1. Unwrap list layers
62
+ def _shell_list(_tp):
63
+ while _is_list(_tp):
64
+ args = getattr(_tp, "__args__", ())
65
+ if args:
66
+ _tp = args[0]
67
+ else:
68
+ break
69
+ return _tp
70
+
71
+ tp = _shell_list(tp)
72
+
73
+ # Alias could wrap a list element, unwrap again
74
+ tp = _unwrap_alias(tp)
75
+
76
+ if tp is type(None): # check again
77
+ return tuple()
78
+
79
+ while True:
80
+ orig = get_origin(tp)
81
+
82
+ if orig in (Union, UnionType):
83
+ args = list(get_args(tp))
84
+ non_none = [a for a in args if a is not type(None)] # noqa: E721
85
+ has_none = len(non_none) != len(args)
86
+ # Optional[T] case -> keep unwrapping (exactly one real type + None)
87
+ if has_none and len(non_none) == 1:
88
+ tp = non_none[0]
89
+ tp = _unwrap_alias(tp)
90
+ tp = _shell_list(tp)
91
+ continue
92
+ # General union: return all non-None members (order preserved)
93
+ if non_none:
94
+ return tuple(non_none)
95
+ return tuple()
96
+ break
97
+
98
+ # single concrete type
99
+ return (tp,)
100
+
101
+
102
+ def get_type_name(anno):
103
+ def name_of(tp):
104
+ origin = get_origin(tp)
105
+ args = get_args(tp)
106
+
107
+ # Annotated[T, ...] -> T
108
+ if origin is Annotated:
109
+ return name_of(args[0]) if args else 'Annotated'
110
+
111
+ # Union / Optional
112
+ if origin is Union:
113
+ non_none = [a for a in args if a is not type(None)]
114
+ if len(non_none) == 1 and len(args) == 2:
115
+ return f"Optional[{name_of(non_none[0])}]"
116
+ return f"Union[{', '.join(name_of(a) for a in args)}]"
117
+
118
+ # Parametrized generics
119
+ if origin is not None:
120
+ origin_name_map = {
121
+ list: 'List',
122
+ dict: 'Dict',
123
+ set: 'Set',
124
+ tuple: 'Tuple',
125
+ frozenset: 'FrozenSet',
126
+ }
127
+ origin_name = origin_name_map.get(origin)
128
+ if origin_name is None:
129
+ origin_name = getattr(origin, '__name__', None) or str(origin).replace('typing.', '')
130
+ if args:
131
+ return f"{origin_name}[{', '.join(name_of(a) for a in args)}]"
132
+ return origin_name
133
+
134
+ # Non-generic leaf types
135
+ if tp is Any:
136
+ return 'Any'
137
+ if tp is None or tp is type(None):
138
+ return 'None'
139
+ if isinstance(tp, type):
140
+ return tp.__name__
141
+
142
+ # ForwardRef
143
+ fwd = getattr(tp, '__forward_arg__', None) or getattr(tp, 'arg', None)
144
+ if fwd:
145
+ return str(fwd)
146
+
147
+ # Fallback clean string
148
+ return str(tp).replace('typing.', '').replace('<class ', '').replace('>', '').replace("'", '')
149
+
150
+ return name_of(anno)
151
+
152
+ def is_inheritance_of_pydantic_base(cls):
153
+ return issubclass(cls, BaseModel) and cls is not BaseModel
154
+
155
+
156
+ def get_bases_fields(schemas: list[type[BaseModel]]) -> set[str]:
157
+ """Collect field names from a list of BaseModel subclasses (their model_fields keys)."""
158
+ fields: set[str] = set()
159
+ for schema in schemas:
160
+ for k, _ in getattr(schema, 'model_fields', {}).items():
161
+ fields.add(k)
162
+ return fields
163
+
164
+
165
+ def get_pydantic_fields(schema: type[BaseModel], bases_fields: set[str]) -> list[FieldInfo]:
166
+ """Extract pydantic model fields with metadata.
167
+
168
+ Parameters:
169
+ schema: The pydantic BaseModel subclass to inspect.
170
+ bases_fields: Set of field names that come from base classes (for from_base marking).
171
+
172
+ Returns:
173
+ A list of FieldInfo objects describing the schema's direct fields.
174
+ """
175
+
176
+ def _is_object(anno): # internal helper, previously a method on Analytics
177
+ _types = get_core_types(anno)
178
+ return any(is_inheritance_of_pydantic_base(t) for t in _types if t)
179
+
180
+ fields: list[FieldInfo] = []
181
+ for k, v in schema.model_fields.items():
182
+ anno = v.annotation
183
+ fields.append(FieldInfo(
184
+ is_object=_is_object(anno),
185
+ name=k,
186
+ from_base=k in bases_fields,
187
+ type_name=get_type_name(anno),
188
+ is_exclude=bool(v.exclude)
189
+ ))
190
+ return fields
191
+
192
+
193
+ def get_vscode_link(kls):
194
+ """Build a VSCode deep link to the class definition.
195
+
196
+ Priority:
197
+ 1. If running inside WSL and WSL_DISTRO_NAME is present, return a remote link:
198
+ vscode://vscode-remote/wsl+<distro>/<absolute/path>:<line>
199
+ (This opens directly in the VSCode WSL remote window.)
200
+ 2. Else, if path is /mnt/<drive>/..., translate to Windows drive and return vscode://file/C:\\...:line
201
+ 3. Else, fallback to vscode://file/<unix-absolute-path>:line
202
+ """
203
+ try:
204
+ source_file = inspect.getfile(kls)
205
+ _lines, start_line = inspect.getsourcelines(kls)
206
+
207
+ distro = os.environ.get("WSL_DISTRO_NAME")
208
+ if distro:
209
+ # Ensure absolute path (it should already be under /) and build remote link
210
+ return f"vscode://vscode-remote/wsl+{distro}{source_file}:{start_line}"
211
+
212
+ # Non-remote scenario: maybe user wants to open via translated Windows path
213
+ if source_file.startswith('/mnt/') and len(source_file) > 6:
214
+ parts = source_file.split('/')
215
+ if len(parts) >= 4 and len(parts[2]) == 1: # drive letter
216
+ drive = parts[2].upper()
217
+ rest = parts[3:]
218
+ win_path = drive + ':\\' + '\\'.join(rest)
219
+ return f"vscode://file/{win_path}:{start_line}"
220
+
221
+ # Fallback plain unix path
222
+ return f"vscode://file/{source_file}:{start_line}"
223
+ except Exception:
224
+ return ""
225
+
226
+
227
+ def get_source(kls):
228
+ try:
229
+ source = inspect.getsource(kls)
230
+ return source
231
+ except Exception:
232
+ return "failed to get source"
@@ -0,0 +1,2 @@
1
+ __all__ = ["__version__"]
2
+ __version__ = "0.4.1"
@@ -0,0 +1,73 @@
1
+ const { defineComponent, ref, watch, onMounted } = window.Vue;
2
+
3
+ // Component: RouteCodeDisplay
4
+ // Props:
5
+ // routeId: route id key in routeItems
6
+ // modelValue: dialog visibility
7
+ // routes: object map { id: { id, name, source_code } }
8
+ export default defineComponent({
9
+ name: 'RouteCodeDisplay',
10
+ props: {
11
+ routeId: { type: String, required: true },
12
+ modelValue: { type: Boolean, default: false },
13
+ routes: { type: Object, default: () => ({}) },
14
+ },
15
+ emits: ['close'],
16
+ setup(props, { emit }) {
17
+ const code = ref('');
18
+ const error = ref(null);
19
+ const link = ref('');
20
+
21
+ function close() { emit('close'); }
22
+
23
+ function highlightLater() {
24
+ requestAnimationFrame(() => {
25
+ try {
26
+ if (window.hljs) {
27
+ const block = document.querySelector('.frv-route-code-display pre code.language-python');
28
+ if (block) {
29
+ window.hljs.highlightElement(block);
30
+ }
31
+ }
32
+ } catch (e) {
33
+ console.warn('highlight failed', e);
34
+ }
35
+ });
36
+ }
37
+
38
+ function load() {
39
+ error.value = null;
40
+ if (!props.routeId) { code.value=''; return; }
41
+ const item = props.routes[props.routeId];
42
+ if (item && item.source_code) {
43
+ code.value = item.source_code;
44
+ link.value = item.vscode_link || '';
45
+ highlightLater();
46
+ } else if (item) {
47
+ code.value = '// no source code available';
48
+ link.value = item.vscode_link || '';
49
+ } else {
50
+ error.value = 'Route not found';
51
+ link.value = '';
52
+ }
53
+ }
54
+
55
+ watch(() => props.modelValue, (v) => { if (v) load(); });
56
+ watch(() => props.routeId, () => { if (props.modelValue) load(); });
57
+
58
+ onMounted(() => { if (props.modelValue) load(); });
59
+
60
+ return { code, error, close, link };
61
+ },
62
+ template: `
63
+ <div class="frv-route-code-display" style="border:1px solid #ccc; position:relative; width:50vw; max-width:50vw; height:100%; background:#fff;">
64
+ <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)" />
65
+ <div v-if="link" class="q-ml-md q-mt-md" style="padding-top:4px;">
66
+ <a :href="link" target="_blank" rel="noopener" style="font-size:12px; color:#3b82f6;">Open in VSCode</a>
67
+ </div>
68
+ <div style="padding:40px 16px 16px 16px; height:100%; box-sizing:border-box; overflow:auto;">
69
+ <div v-if="error" style="color:#c10015; font-family:Menlo, monospace; font-size:12px;">{{ error }}</div>
70
+ <pre v-else style="margin:0;"><code class="language-python">{{ code }}</code></pre>
71
+ </div>
72
+ </div>`
73
+ });
@@ -0,0 +1,152 @@
1
+ const { defineComponent, ref, watch, onMounted } = window.Vue;
2
+
3
+ // Component: SchemaCodeDisplay
4
+ // Props:
5
+ // schemaName: full qualified schema id (module.Class)
6
+ // modelValue: boolean (dialog visibility from parent)
7
+ // source: optional direct source code (if already resolved client side)
8
+ // schemas: list of schema meta objects (each containing fullname & source_code)
9
+ // Behavior:
10
+ // - When dialog opens and schemaName changes, search schemas prop and display its source_code.
11
+ // - No network / global cache side effects.
12
+ export default defineComponent({
13
+ name: "SchemaCodeDisplay",
14
+ props: {
15
+ schemaName: { type: String, required: true },
16
+ modelValue: { type: Boolean, default: false },
17
+ source: { type: String, default: null },
18
+ schemas: { type: Array, default: () => [] },
19
+ },
20
+ emits: ["close"],
21
+ setup(props, { emit }) {
22
+ const loading = ref(false);
23
+ const code = ref("");
24
+ const link = ref("");
25
+ const error = ref(null);
26
+ const fields = ref([]); // schema fields list
27
+ const tab = ref('source');
28
+
29
+ function close() {
30
+ emit("close");
31
+ }
32
+
33
+ function highlightLater() {
34
+ // wait a tick for DOM update
35
+ requestAnimationFrame(() => {
36
+ try {
37
+ if (window.hljs) {
38
+ const block = document.querySelector(
39
+ ".frv-code-display pre code.language-python"
40
+ );
41
+ if (block) {
42
+ window.hljs.highlightElement(block);
43
+ }
44
+ }
45
+ } catch (e) {
46
+ console.warn("highlight failed", e);
47
+ }
48
+ });
49
+ }
50
+
51
+ function loadSource() {
52
+ if (!props.schemaName) return;
53
+ if (props.source) {
54
+ code.value = props.source;
55
+ highlightLater();
56
+ return;
57
+ }
58
+ loading.value = true;
59
+ error.value = null;
60
+ try {
61
+ const item = props.schemas.find((s) => s.fullname === props.schemaName);
62
+ if (item) {
63
+ link.value = item.vscode_link || "";
64
+ code.value = item.source_code || "// no source code available";
65
+ // capture fields if provided
66
+ fields.value = Array.isArray(item.fields) ? item.fields : [];
67
+ highlightLater();
68
+ } else {
69
+ error.value = "Schema not found";
70
+ }
71
+ } catch (e) {
72
+ error.value = "Failed to load source";
73
+ } finally {
74
+ loading.value = false;
75
+ }
76
+ }
77
+
78
+ // re-highlight when switching back to source tab
79
+ watch(
80
+ () => tab.value,
81
+ (val) => {
82
+ if (val === 'source') {
83
+ highlightLater();
84
+ }
85
+ }
86
+ );
87
+
88
+ watch(
89
+ () => props.modelValue,
90
+ (val) => {
91
+ if (val) {
92
+ loadSource();
93
+ }
94
+ }
95
+ );
96
+
97
+ onMounted(() => {
98
+ if (props.modelValue) loadSource();
99
+ });
100
+
101
+ return { loading, link, code, error, close, fields, tab };
102
+ },
103
+ template: `
104
+ <div class="frv-code-display" style="border: 1px solid #ccc; border-left: none; position:relative; width:40vw; max-width:40vw; height:100%; background:#fff;">
105
+ <q-btn dense flat round icon="close" @click="close" aria-label="Close"
106
+ style="position:absolute; top:6px; right:6px; z-index:10; background:rgba(255,255,255,0.85)" />
107
+ <div v-if="link" class="q-ml-md q-mt-md">
108
+ <a :href="link" target="_blank" rel="noopener" style="font-size:12px; color:#3b82f6;">
109
+ Open in VSCode
110
+ </a>
111
+ </div>
112
+
113
+ <div style="padding:8px 12px 0 12px; box-sizing:border-box;">
114
+ <q-tabs v-model="tab" align="left" dense active-color="primary" indicator-color="primary" class="text-grey-8">
115
+ <q-tab name="source" label="Source Code" />
116
+ <q-tab name="fields" label="Fields" />
117
+ </q-tabs>
118
+ </div>
119
+ <q-separator />
120
+ <div style="padding:8px 16px 16px 16px; height:75%; box-sizing:border-box; overflow:auto;">
121
+ <div v-if="loading" style="font-family:Menlo, monospace; font-size:12px;">Loading source...</div>
122
+ <div v-else-if="error" style="color:#c10015; font-family:Menlo, monospace; font-size:12px;">{{ error }}</div>
123
+ <template v-else>
124
+ <div v-show="tab === 'source'">
125
+ <pre style="margin:0;"><code class="language-python">{{ code }}</code></pre>
126
+ </div>
127
+ <div v-show="tab === 'fields'">
128
+ <table style="border-collapse:collapse; width:100%; font-size:12px; font-family:Menlo, monospace;">
129
+ <thead>
130
+ <tr>
131
+ <th style="text-align:left; border-bottom:1px solid #ddd; padding:4px 6px;">Field</th>
132
+ <th style="text-align:left; border-bottom:1px solid #ddd; padding:4px 6px;">Type</th>
133
+ <th style="text-align:left; border-bottom:1px solid #ddd; padding:4px 6px;">From Base</th>
134
+ </tr>
135
+ </thead>
136
+ <tbody>
137
+ <tr v-for="f in fields" :key="f.name">
138
+ <td style="padding:4px 6px; border-bottom:1px solid #f0f0f0;">{{ f.name }}</td>
139
+ <td style="padding:4px 6px; border-bottom:1px solid #f0f0f0; white-space:nowrap;">{{ f.type_name }}</td>
140
+ <td style="padding:4px 6px; border-bottom:1px solid #f0f0f0; text-align:left;">{{ f.from_base ? '✔︎' : '' }}</td>
141
+ </tr>
142
+ <tr v-if="!fields.length">
143
+ <td colspan="3" style="padding:8px 6px; color:#666; font-style:italic;">No fields</td>
144
+ </tr>
145
+ </tbody>
146
+ </table>
147
+ </div>
148
+ </template>
149
+ </div>
150
+ </div>
151
+ `,
152
+ });
@@ -0,0 +1,189 @@
1
+ import { GraphUI } from "../graph-ui.js";
2
+ const { defineComponent, reactive, ref, onMounted, nextTick, watch } =
3
+ window.Vue;
4
+
5
+ // SchemaFieldFilter component
6
+ // Features:
7
+ // - Fetch initial schemas list (GET /dot) and build schema options
8
+ // - Second selector lists fields of the chosen schema
9
+ // - Query button disabled until a schema is selected
10
+ // - On query: POST /dot with schema_name + optional schema_field; render returned DOT in #graph-schema-field
11
+ // - Uses GraphUI once and re-renders
12
+ // - Emits 'queried' event after successful render (payload: { schemaName, fieldName })
13
+ export default defineComponent({
14
+ name: "SchemaFieldFilter",
15
+ props: {
16
+ schemaName: { type: String, default: null }, // external injection triggers auto-query
17
+ schemas: { type: Array, default: () => [] }, // externally provided schemas (state.rawSchemasFull or similar)
18
+ },
19
+ emits: ["queried", "close"],
20
+ setup(props, { emit }) {
21
+ const state = reactive({
22
+ loadingSchemas: false,
23
+ querying: false,
24
+ schemas: [], // [{ name, fullname, fields: [{name,...}] }]
25
+ schemaOptions: [], // [{ label, value }]
26
+ fieldOptions: [], // [ field.name ]
27
+ schemaFullname: null,
28
+ fieldName: null,
29
+ error: null,
30
+ showFields: "object",
31
+ showFieldOptions: [
32
+ { label: "No fields", value: "single" },
33
+ { label: "Object fields", value: "object" },
34
+ { label: "All fields", value: "all" },
35
+ ],
36
+ });
37
+
38
+ let graphInstance = null;
39
+ let lastAppliedExternal = null;
40
+
41
+ async function loadSchemas() {
42
+ // Refactored: use externally provided props.schemas directly; no network call.
43
+ state.error = null;
44
+ state.schemas = Array.isArray(props.schemas) ? props.schemas : [];
45
+ state.schemaOptions = state.schemas.map((s) => ({
46
+ label: `${s.name} (${s.fullname})`,
47
+ value: s.fullname,
48
+ }));
49
+ // Maintain compatibility: loadingSchemas flag toggled quickly (no async work)
50
+ state.loadingSchemas = false;
51
+ }
52
+
53
+ function onFilterSchemas(val, update) {
54
+ const needle = (val || "").toLowerCase();
55
+ update(() => {
56
+ let opts = state.schemas.map((s) => ({
57
+ label: `${s.name} (${s.fullname})`,
58
+ value: s.fullname,
59
+ }));
60
+ if (needle) {
61
+ opts = opts.filter((o) => o.label.toLowerCase().includes(needle));
62
+ }
63
+ state.schemaOptions = opts;
64
+ });
65
+ }
66
+
67
+ function onSchemaChange(val) {
68
+ state.schemaFullname = val;
69
+ state.fieldName = null;
70
+ const schema = state.schemas.find((s) => s.fullname === val);
71
+ state.fieldOptions = schema ? schema.fields.map((f) => f.name) : [];
72
+ }
73
+
74
+ async function onQuery() {
75
+ if (!state.schemaFullname) return;
76
+ state.querying = true;
77
+ state.error = null;
78
+ try {
79
+ const payload = {
80
+ schema_name: state.schemaFullname,
81
+ schema_field: state.fieldName || null,
82
+ show_fields: state.showFields,
83
+ };
84
+ const res = await fetch("/dot", {
85
+ method: "POST",
86
+ headers: { "Content-Type": "application/json" },
87
+ body: JSON.stringify(payload),
88
+ });
89
+ const dotText = await res.text();
90
+ if (!graphInstance) {
91
+ graphInstance = new GraphUI("#graph-schema-field");
92
+ }
93
+ await graphInstance.render(dotText);
94
+ emit("queried", {
95
+ schemaName: state.schemaFullname,
96
+ fieldName: state.fieldName,
97
+ });
98
+ } catch (e) {
99
+ state.error = "Query failed";
100
+ console.error("SchemaFieldFilter query failed", e);
101
+ } finally {
102
+ state.querying = false;
103
+ }
104
+ }
105
+
106
+ function applyExternalSchema(name) {
107
+ if (!name || !state.schemas.length) return;
108
+ if (lastAppliedExternal === name) return; // avoid duplicate
109
+ const schema = state.schemas.find((s) => s.fullname === name);
110
+ if (!schema) return;
111
+ state.schemaFullname = schema.fullname;
112
+ state.fieldOptions = schema.fields.map((f) => f.name);
113
+ state.fieldName = null; // reset field for external injection
114
+ lastAppliedExternal = name;
115
+ // auto query
116
+ onQuery();
117
+ }
118
+
119
+ onMounted(async () => {
120
+ await nextTick();
121
+ await loadSchemas();
122
+ if (props.schemaName) {
123
+ applyExternalSchema(props.schemaName);
124
+ }
125
+ });
126
+
127
+
128
+ function close() {
129
+ emit("close");
130
+ }
131
+
132
+ return { state, onSchemaChange, onQuery, close, onFilterSchemas };
133
+ },
134
+ template: `
135
+ <div style="height:100%; position:relative; background:#fff;">
136
+ <div style="position:absolute; top:8px; left:8px; z-index:10; background:rgba(255,255,255,0.95); padding:8px 10px; border-radius:4px; box-shadow:0 1px 3px rgba(0,0,0,0.15);" class="q-gutter-sm row items-center">
137
+ <q-select
138
+ dense outlined use-input input-debounce="0"
139
+ v-model="state.schemaFullname"
140
+ :options="state.schemaOptions"
141
+ option-label="label"
142
+ option-value="value"
143
+ emit-value
144
+ map-options
145
+ :loading="state.loadingSchemas"
146
+ style="min-width:220px"
147
+ clearable
148
+ label="Select schema"
149
+ @update:model-value="onSchemaChange"
150
+ @filter="onFilterSchemas"
151
+ />
152
+ <q-select
153
+ dense outlined
154
+ v-model="state.fieldName"
155
+ :disable="!state.schemaFullname || state.fieldOptions.length===0"
156
+ :options="state.fieldOptions"
157
+ style="min-width:180px"
158
+ clearable
159
+ label="Select field (optional)"
160
+ />
161
+ <q-option-group
162
+ v-model="state.showFields"
163
+ :options="state.showFieldOptions"
164
+ type="radio"
165
+ inline
166
+ dense
167
+ color="primary"
168
+ style="min-width:260px"
169
+ />
170
+ <q-btn
171
+ class="q-ml-md"
172
+ icon="search"
173
+ label="Search"
174
+ outline
175
+ :disable="!state.schemaFullname"
176
+ :loading="state.querying"
177
+ @click="onQuery" />
178
+ </div>
179
+ <q-btn
180
+ flat dense round icon="close"
181
+ aria-label="Close"
182
+ @click="close"
183
+ style="position:absolute; top:6px; right:6px; z-index:11; background:rgba(255,255,255,0.85);"
184
+ />
185
+ <div v-if="state.error" style="position:absolute; top:52px; left:8px; z-index:10; color:#c10015; font-size:12px;">{{ state.error }}</div>
186
+ <div id="graph-schema-field" style="width:100%; height:100%; overflow:auto; background:#fafafa"></div>
187
+ </div>
188
+ `,
189
+ });