quickdbd-cli 0.1.4__tar.gz → 0.1.6__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.
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/.gitignore +2 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/PKG-INFO +13 -5
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/README.md +9 -4
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/pyproject.toml +2 -1
- quickdbd_cli-0.1.6/src/quickdbd_cli/__init__.py +1 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/assets/render.js +116 -2
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/assets/style.css +27 -2
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/assets/viewer.js +108 -10
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/cli.py +93 -11
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/dbt.py +133 -3
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/export.py +16 -4
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/html.py +3 -2
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/layout.py +54 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/project.py +7 -1
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/tests/test_dbt.py +93 -0
- quickdbd_cli-0.1.6/tests/test_path.py +67 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/tests/test_pipeline.py +58 -0
- quickdbd_cli-0.1.4/src/quickdbd_cli/__init__.py +0 -1
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/LICENSE +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/NOTICE +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/examples/quickdbd-sample.txt +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/examples/synthetic/finance.txt +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/examples/synthetic/hr.txt +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/examples/synthetic/inventory.txt +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/examples/synthetic/marketing.txt +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/examples/synthetic/qdbd.layout.json +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/examples/synthetic/sales.txt +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/scripts/gen_font_metrics.py +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/scripts/gen_schema.py +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/scripts/release.sh +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/__main__.py +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/assets/Inter-Bold.ttf +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/assets/Inter-Bold.woff2 +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/assets/Inter-LICENSE.txt +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/assets/Inter-Regular.ttf +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/assets/Inter-Regular.woff2 +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/assets/editor.js +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/assets/elk-LICENSE.md +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/assets/elk.bundled.js +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/assets/font-metrics.json +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/diff.py +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/jsrt.py +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/measure.py +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/model.py +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/parser.py +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/src/quickdbd_cli/serve.py +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/tests/fixtures/jaffle/LICENSE +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/tests/fixtures/jaffle/README.md +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/tests/fixtures/jaffle/catalog.json +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/tests/fixtures/jaffle/manifest.json +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/tests/test_diff.py +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/tests/test_parser.py +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/tests/test_select.py +0 -0
- {quickdbd_cli-0.1.4 → quickdbd_cli-0.1.6}/tests/test_serve.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: quickdbd-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Compile QuickDBD-syntax schema files into interactive HTML ER diagrams, with no table limit.
|
|
5
5
|
Project-URL: Homepage, https://github.com/MarkPhamm/quickdbd-cli
|
|
6
6
|
Author: Minh (Mark) Pham
|
|
@@ -21,6 +21,9 @@ Provides-Extra: dev
|
|
|
21
21
|
Requires-Dist: fonttools>=4.50; extra == 'dev'
|
|
22
22
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
23
23
|
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
24
|
+
Provides-Extra: docs
|
|
25
|
+
Requires-Dist: mkdocs-material<10,>=9.5; extra == 'docs'
|
|
26
|
+
Requires-Dist: mkdocs<2,>=1.6; extra == 'docs'
|
|
24
27
|
Description-Content-Type: text/markdown
|
|
25
28
|
|
|
26
29
|
# quickdbd-cli (`qdbd`)
|
|
@@ -44,7 +47,7 @@ pipx install quickdbd-cli # or: uvx --from quickdbd-cli qdbd ...
|
|
|
44
47
|
qdbd build schema.txt -o schema.html
|
|
45
48
|
```
|
|
46
49
|
|
|
47
|
-
**Live demo (200 tables):** https://markphamm.github.io/quickdbd-cli/
|
|
50
|
+
**Docs:** https://markphamm.github.io/quickdbd-cli/ · **Live demo (200 tables):** https://markphamm.github.io/quickdbd-cli/demo/
|
|
48
51
|
|
|
49
52
|
Not affiliated with QuickDBD. It reads the same text syntax, so schemas move freely between the two.
|
|
50
53
|
|
|
@@ -60,6 +63,8 @@ Not affiliated with QuickDBD. It reads the same text syntax, so schemas move fre
|
|
|
60
63
|
- hover a column to highlight its relationship
|
|
61
64
|
- hover a dotted-underlined table or column to read its `#` doc comments
|
|
62
65
|
- PNG/SVG export buttons, and **draw.io**: an editable diagrams.net file using draw.io's ER table shapes and crow's-foot arrows
|
|
66
|
+
- **Path finder:** in a table's detail panel, type another table to see the shortest join path, highlighted on the canvas, with a ready-to-paste SQL `JOIN` (also `qdbd path erd/ --from payments --to customers`)
|
|
67
|
+
- **Regions:** a shaded, labelled area per file or dbt folder (toolbar toggle). `--by-group` on `build` / `export` / `layout` arranges tables so each folder sits together
|
|
63
68
|
- shareable links: the URL keeps the filter, selected table, collapse mode and zoom; **Link** copies it
|
|
64
69
|
- **Live editing:** `qdbd serve` reloads when you save, has a browser editor pane with autosave, and remembers where you drag tables.
|
|
65
70
|
- **Many files, one diagram:** point it at a folder and foreign keys can cross files.
|
|
@@ -78,6 +83,8 @@ qdbd build erd/ -o erd.html
|
|
|
78
83
|
- **Relationships:** `relationships` tests (including dbt_utils `relationships_where`) and `foreign_key` constraints.
|
|
79
84
|
- **Primary keys:** `primary_key` constraints, or else a column with both `unique` and `not_null` tests.
|
|
80
85
|
- **Docs:** model and column descriptions become doc comments, shown as tooltips.
|
|
86
|
+
- **`--infer-fks`:** also links `customer_id`-style columns to the table they name, when no relationship is declared. Ambiguous matches are skipped, never guessed.
|
|
87
|
+
- **`--check`:** in CI, fails when the committed files are out of date.
|
|
81
88
|
- **Options:**
|
|
82
89
|
- `--select 'marts.*'` narrows by name, fqn or path (repeatable)
|
|
83
90
|
- `--package all` includes installed packages
|
|
@@ -129,11 +136,12 @@ The selector `diff:modified` (or `diff:added` / `diff:changed`) works like dbt's
|
|
|
129
136
|
|
|
130
137
|
```bash
|
|
131
138
|
qdbd check PATH... [--strict] # validate; "No errors found. Good job!"
|
|
132
|
-
qdbd build PATH... -o diagram.html # shareable interactive HTML
|
|
139
|
+
qdbd build PATH... -o diagram.html # shareable interactive HTML
|
|
133
140
|
qdbd serve PATH... [--port 8765] # live editor + drag-to-arrange
|
|
134
141
|
qdbd export PATH... -o out.png [--focus Customer --depth 1] # PNG, SVG or .drawio
|
|
135
142
|
qdbd layout PATH... [--reset] # (re)write the layout file
|
|
136
143
|
qdbd import dbt target/manifest.json -o erd/ # generate schema files from a dbt project
|
|
144
|
+
qdbd path PATH... --from A --to B # shortest join path + SQL JOIN snippet
|
|
137
145
|
```
|
|
138
146
|
|
|
139
147
|
`PATH` is a `.txt` file or a folder. Every `*.txt` under a folder is read recursively.
|
|
@@ -177,7 +185,7 @@ jobs:
|
|
|
177
185
|
permissions: { contents: read, pull-requests: write }
|
|
178
186
|
steps:
|
|
179
187
|
- uses: actions/checkout@v4
|
|
180
|
-
- uses: MarkPhamm/quickdbd-cli@v0.1.
|
|
188
|
+
- uses: MarkPhamm/quickdbd-cli@v0.1.6
|
|
181
189
|
with:
|
|
182
190
|
path: erd/ # schema file(s) or folder(s)
|
|
183
191
|
output: site/index.html
|
|
@@ -201,7 +209,7 @@ Outputs are `html`, `changed` and `diff-file`.
|
|
|
201
209
|
```yaml
|
|
202
210
|
repos:
|
|
203
211
|
- repo: https://github.com/MarkPhamm/quickdbd-cli
|
|
204
|
-
rev: v0.1.
|
|
212
|
+
rev: v0.1.6
|
|
205
213
|
hooks:
|
|
206
214
|
- id: qdbd-check
|
|
207
215
|
files: ^erd/.*\.txt$ # point at your schema files
|
|
@@ -19,7 +19,7 @@ pipx install quickdbd-cli # or: uvx --from quickdbd-cli qdbd ...
|
|
|
19
19
|
qdbd build schema.txt -o schema.html
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
**Live demo (200 tables):** https://markphamm.github.io/quickdbd-cli/
|
|
22
|
+
**Docs:** https://markphamm.github.io/quickdbd-cli/ · **Live demo (200 tables):** https://markphamm.github.io/quickdbd-cli/demo/
|
|
23
23
|
|
|
24
24
|
Not affiliated with QuickDBD. It reads the same text syntax, so schemas move freely between the two.
|
|
25
25
|
|
|
@@ -35,6 +35,8 @@ Not affiliated with QuickDBD. It reads the same text syntax, so schemas move fre
|
|
|
35
35
|
- hover a column to highlight its relationship
|
|
36
36
|
- hover a dotted-underlined table or column to read its `#` doc comments
|
|
37
37
|
- PNG/SVG export buttons, and **draw.io**: an editable diagrams.net file using draw.io's ER table shapes and crow's-foot arrows
|
|
38
|
+
- **Path finder:** in a table's detail panel, type another table to see the shortest join path, highlighted on the canvas, with a ready-to-paste SQL `JOIN` (also `qdbd path erd/ --from payments --to customers`)
|
|
39
|
+
- **Regions:** a shaded, labelled area per file or dbt folder (toolbar toggle). `--by-group` on `build` / `export` / `layout` arranges tables so each folder sits together
|
|
38
40
|
- shareable links: the URL keeps the filter, selected table, collapse mode and zoom; **Link** copies it
|
|
39
41
|
- **Live editing:** `qdbd serve` reloads when you save, has a browser editor pane with autosave, and remembers where you drag tables.
|
|
40
42
|
- **Many files, one diagram:** point it at a folder and foreign keys can cross files.
|
|
@@ -53,6 +55,8 @@ qdbd build erd/ -o erd.html
|
|
|
53
55
|
- **Relationships:** `relationships` tests (including dbt_utils `relationships_where`) and `foreign_key` constraints.
|
|
54
56
|
- **Primary keys:** `primary_key` constraints, or else a column with both `unique` and `not_null` tests.
|
|
55
57
|
- **Docs:** model and column descriptions become doc comments, shown as tooltips.
|
|
58
|
+
- **`--infer-fks`:** also links `customer_id`-style columns to the table they name, when no relationship is declared. Ambiguous matches are skipped, never guessed.
|
|
59
|
+
- **`--check`:** in CI, fails when the committed files are out of date.
|
|
56
60
|
- **Options:**
|
|
57
61
|
- `--select 'marts.*'` narrows by name, fqn or path (repeatable)
|
|
58
62
|
- `--package all` includes installed packages
|
|
@@ -104,11 +108,12 @@ The selector `diff:modified` (or `diff:added` / `diff:changed`) works like dbt's
|
|
|
104
108
|
|
|
105
109
|
```bash
|
|
106
110
|
qdbd check PATH... [--strict] # validate; "No errors found. Good job!"
|
|
107
|
-
qdbd build PATH... -o diagram.html # shareable interactive HTML
|
|
111
|
+
qdbd build PATH... -o diagram.html # shareable interactive HTML
|
|
108
112
|
qdbd serve PATH... [--port 8765] # live editor + drag-to-arrange
|
|
109
113
|
qdbd export PATH... -o out.png [--focus Customer --depth 1] # PNG, SVG or .drawio
|
|
110
114
|
qdbd layout PATH... [--reset] # (re)write the layout file
|
|
111
115
|
qdbd import dbt target/manifest.json -o erd/ # generate schema files from a dbt project
|
|
116
|
+
qdbd path PATH... --from A --to B # shortest join path + SQL JOIN snippet
|
|
112
117
|
```
|
|
113
118
|
|
|
114
119
|
`PATH` is a `.txt` file or a folder. Every `*.txt` under a folder is read recursively.
|
|
@@ -152,7 +157,7 @@ jobs:
|
|
|
152
157
|
permissions: { contents: read, pull-requests: write }
|
|
153
158
|
steps:
|
|
154
159
|
- uses: actions/checkout@v4
|
|
155
|
-
- uses: MarkPhamm/quickdbd-cli@v0.1.
|
|
160
|
+
- uses: MarkPhamm/quickdbd-cli@v0.1.6
|
|
156
161
|
with:
|
|
157
162
|
path: erd/ # schema file(s) or folder(s)
|
|
158
163
|
output: site/index.html
|
|
@@ -176,7 +181,7 @@ Outputs are `html`, `changed` and `diff-file`.
|
|
|
176
181
|
```yaml
|
|
177
182
|
repos:
|
|
178
183
|
- repo: https://github.com/MarkPhamm/quickdbd-cli
|
|
179
|
-
rev: v0.1.
|
|
184
|
+
rev: v0.1.6
|
|
180
185
|
hooks:
|
|
181
186
|
- id: qdbd-check
|
|
182
187
|
files: ^erd/.*\.txt$ # point at your schema files
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quickdbd-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6"
|
|
8
8
|
description = "Compile QuickDBD-syntax schema files into interactive HTML ER diagrams, with no table limit."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -25,6 +25,7 @@ dependencies = [
|
|
|
25
25
|
|
|
26
26
|
[project.optional-dependencies]
|
|
27
27
|
dev = ["pytest>=8", "ruff>=0.6", "fonttools>=4.50"]
|
|
28
|
+
docs = ["mkdocs>=1.6,<2", "mkdocs-material>=9.5,<10"]
|
|
28
29
|
|
|
29
30
|
[project.scripts]
|
|
30
31
|
qdbd = "quickdbd_cli.cli:main"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.6"
|
|
@@ -178,6 +178,41 @@
|
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/* ------------------------------------------------------------------ regions: one shaded area per group */
|
|
182
|
+
var REGION_COLORS = ["#4c78a8", "#59a14f", "#e15759", "#b07aa1", "#f28e2b", "#76b7b2", "#9c755f", "#edc948"];
|
|
183
|
+
var REGION_PAD = 28, REGION_LABEL = 30;
|
|
184
|
+
function regions(model, pos, mode) {
|
|
185
|
+
var geo = geometry(model, mode), groups = {}, names = [];
|
|
186
|
+
model.tables.forEach(function (t) {
|
|
187
|
+
var g = t.group || "", p = pos[t.id];
|
|
188
|
+
if (!g || !p) return;
|
|
189
|
+
if (!groups[g]) { groups[g] = [Infinity, Infinity, -Infinity, -Infinity]; names.push(g); }
|
|
190
|
+
var b = groups[g], tg = geo[t.id];
|
|
191
|
+
b[0] = Math.min(b[0], p[0]); b[1] = Math.min(b[1], p[1]);
|
|
192
|
+
b[2] = Math.max(b[2], p[0] + tg.w); b[3] = Math.max(b[3], p[1] + tg.h);
|
|
193
|
+
});
|
|
194
|
+
var all = {};
|
|
195
|
+
model.tables.forEach(function (t) { if (t.group) all[t.group] = true; });
|
|
196
|
+
var order = Object.keys(all).sort();
|
|
197
|
+
return names.sort().map(function (g) {
|
|
198
|
+
var b = groups[g];
|
|
199
|
+
return {
|
|
200
|
+
name: g,
|
|
201
|
+
color: REGION_COLORS[order.indexOf(g) % REGION_COLORS.length],
|
|
202
|
+
x: b[0] - REGION_PAD, y: b[1] - REGION_PAD - REGION_LABEL,
|
|
203
|
+
w: b[2] - b[0] + 2 * REGION_PAD, h: b[3] - b[1] + 2 * REGION_PAD + REGION_LABEL,
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function renderRegions(model, pos, mode) {
|
|
208
|
+
return regions(model, pos, mode).map(function (r) {
|
|
209
|
+
return '<g class="rg" data-g="' + esc(r.name) + '"><rect x="' + n(r.x) + '" y="' + n(r.y) + '" width="' + n(r.w) + '" height="' + n(r.h) +
|
|
210
|
+
'" rx="14" fill="' + r.color + '" fill-opacity="0.07" stroke="' + r.color + '" stroke-opacity="0.45" stroke-width="1.5"/>' +
|
|
211
|
+
'<text x="' + n(r.x + 16) + '" y="' + n(r.y + 22) + '" fill="' + r.color + '" font-family="' + FONT +
|
|
212
|
+
'" font-size="16" font-weight="bold">' + esc(r.name) + "</text></g>";
|
|
213
|
+
}).join("");
|
|
214
|
+
}
|
|
215
|
+
|
|
181
216
|
function renderEdges(model, pos, mode, geo) {
|
|
182
217
|
geo = geo || geometry(model, mode);
|
|
183
218
|
return model.rels.map(function (r) { return renderEdge(model, geo, pos, r); }).join("");
|
|
@@ -186,8 +221,9 @@
|
|
|
186
221
|
/* opts.docMarks: underline tables/columns that have doc comments (viewer only, not exports). */
|
|
187
222
|
function renderInner(model, pos, mode, opts) {
|
|
188
223
|
var geo = geometry(model, mode), marks = !!(opts && opts.docMarks);
|
|
224
|
+
var rg = opts && opts.regions ? '<g class="regions">' + renderRegions(model, pos, mode) + "</g>" : "";
|
|
189
225
|
var tables = model.tables.map(function (t) { return pos[t.id] ? renderTable(model, t, geo[t.id], pos[t.id], marks) : ""; }).join("");
|
|
190
|
-
return '<g class="edges">' + renderEdges(model, pos, mode, geo) + '</g><g class="tables">' + tables + "</g>";
|
|
226
|
+
return rg + '<g class="edges">' + renderEdges(model, pos, mode, geo) + '</g><g class="tables">' + tables + "</g>";
|
|
191
227
|
}
|
|
192
228
|
|
|
193
229
|
function bounds(model, pos, mode) {
|
|
@@ -208,11 +244,16 @@
|
|
|
208
244
|
var mode = opts.mode || "full", m = subset(model, opts.only), pad = opts.pad == null ? 40 : opts.pad;
|
|
209
245
|
var edgePad = 60;
|
|
210
246
|
var b = bounds(m, pos, mode);
|
|
247
|
+
if (opts.regions) {
|
|
248
|
+
regions(m, pos, mode).forEach(function (r) {
|
|
249
|
+
b = [Math.min(b[0], r.x), Math.min(b[1], r.y), Math.max(b[2], r.x + r.w), Math.max(b[3], r.y + r.h)];
|
|
250
|
+
});
|
|
251
|
+
}
|
|
211
252
|
var x0 = b[0] - pad - edgePad, y0 = b[1] - pad, w = b[2] - b[0] + 2 * (pad + edgePad), h = b[3] - b[1] + 2 * pad;
|
|
212
253
|
var style = opts.fontCss ? "<style>" + opts.fontCss + "</style>" : "";
|
|
213
254
|
return '<svg xmlns="http://www.w3.org/2000/svg" width="' + n(w) + '" height="' + n(h) + '" viewBox="' + n(x0) + " " + n(y0) + " " + n(w) + " " + n(h) + '">' +
|
|
214
255
|
style + '<rect x="' + n(x0) + '" y="' + n(y0) + '" width="' + n(w) + '" height="' + n(h) + '" fill="' + C.bg + '"/>' +
|
|
215
|
-
renderInner(m, pos, mode) + "</svg>";
|
|
256
|
+
renderInner(m, pos, mode, { regions: opts.regions }) + "</svg>";
|
|
216
257
|
}
|
|
217
258
|
|
|
218
259
|
/* Tables within `depth` relationship hops of `focus`. */
|
|
@@ -252,6 +293,15 @@
|
|
|
252
293
|
}
|
|
253
294
|
function n2(v) { return Math.round(v); }
|
|
254
295
|
var b = bounds(m, pos, mode);
|
|
296
|
+
if (opts.regions) {
|
|
297
|
+
regions(m, pos, mode).forEach(function (r) {
|
|
298
|
+
cells.push(cell({
|
|
299
|
+
id: id(), value: r.name, vertex: "1", parent: "1",
|
|
300
|
+
style: "rounded=1;arcSize=4;whiteSpace=wrap;html=0;fillColor=" + r.color + ";opacity=12;strokeColor=" + r.color +
|
|
301
|
+
";verticalAlign=top;align=left;spacingLeft=12;spacingTop=4;fontStyle=1;fontSize=16;fontColor=" + r.color + ";",
|
|
302
|
+
}, g(r.x - b[0] + 40, r.y - b[1] + 40, r.w + KEYW, r.h)));
|
|
303
|
+
});
|
|
304
|
+
}
|
|
255
305
|
m.tables.forEach(function (t) {
|
|
256
306
|
var p = pos[t.id];
|
|
257
307
|
if (!p) return;
|
|
@@ -377,8 +427,70 @@
|
|
|
377
427
|
return model.tables.filter(function (t) { return (inc === null || inc[t.id]) && !exc[t.id]; }).map(function (t) { return t.id; });
|
|
378
428
|
}
|
|
379
429
|
|
|
430
|
+
/* ------------------------------------------------------------------ path finder
|
|
431
|
+
* Shortest chain of relationships between two tables (relationships are walked in either
|
|
432
|
+
* direction). Each step says which columns join. */
|
|
433
|
+
function findPath(model, from, to) {
|
|
434
|
+
if (from === to) return [];
|
|
435
|
+
var adj = {};
|
|
436
|
+
model.rels.forEach(function (r, i) {
|
|
437
|
+
(adj[r.a.t] = adj[r.a.t] || []).push({ to: r.b.t, i: i, fwd: true });
|
|
438
|
+
(adj[r.b.t] = adj[r.b.t] || []).push({ to: r.a.t, i: i, fwd: false });
|
|
439
|
+
});
|
|
440
|
+
var prev = {}, queue = [from], seen = {};
|
|
441
|
+
seen[from] = true;
|
|
442
|
+
while (queue.length) {
|
|
443
|
+
var cur = queue.shift();
|
|
444
|
+
if (cur === to) break;
|
|
445
|
+
(adj[cur] || []).forEach(function (e) {
|
|
446
|
+
if (seen[e.to]) return;
|
|
447
|
+
seen[e.to] = true;
|
|
448
|
+
prev[e.to] = { from: cur, e: e };
|
|
449
|
+
queue.push(e.to);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
if (!seen[to]) return null;
|
|
453
|
+
var steps = [], at = to;
|
|
454
|
+
while (at !== from) {
|
|
455
|
+
var pv = prev[at], r = model.rels[pv.e.i];
|
|
456
|
+
var here = pv.e.fwd ? r.a : r.b, there = pv.e.fwd ? r.b : r.a; // here = side of pv.from
|
|
457
|
+
steps.unshift({ rel: pv.e.i, from: { t: pv.from, c: here.c, card: here.card }, to: { t: at, c: there.c, card: there.card } });
|
|
458
|
+
at = pv.from;
|
|
459
|
+
}
|
|
460
|
+
return steps;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
var SQL_RESERVED = ("all and any as asc between by case check column constraint create cross default delete desc distinct " +
|
|
464
|
+
"drop else end exists foreign from full grant group having in index inner insert intersect into is join key left like " +
|
|
465
|
+
"limit not null offset on or order outer primary references right select set table then to union unique update user " +
|
|
466
|
+
"using values view when where with").split(" ");
|
|
467
|
+
function sqlIdent(name) {
|
|
468
|
+
var plain = /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && SQL_RESERVED.indexOf(name.toLowerCase()) < 0;
|
|
469
|
+
return plain ? name : '"' + name.replace(/"/g, '""') + '"';
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* SQL FROM/JOIN clause following a path. */
|
|
473
|
+
function pathSql(model, from, steps) {
|
|
474
|
+
var byId = {};
|
|
475
|
+
model.tables.forEach(function (t) { byId[t.id] = t; });
|
|
476
|
+
function col(t, c) {
|
|
477
|
+
var x = byId[t].cols.filter(function (k) { return k.id === c; })[0];
|
|
478
|
+
return x ? sqlIdent(x.name) : null;
|
|
479
|
+
}
|
|
480
|
+
var lines = ["FROM " + sqlIdent(byId[from].name)];
|
|
481
|
+
steps.forEach(function (s) {
|
|
482
|
+
var a = byId[s.from.t].name, b = byId[s.to.t].name, ca = col(s.from.t, s.from.c), cb = col(s.to.t, s.to.c);
|
|
483
|
+
if (ca && cb) lines.push("JOIN " + sqlIdent(b) + " ON " + sqlIdent(b) + "." + cb + " = " + sqlIdent(a) + "." + ca);
|
|
484
|
+
else lines.push("JOIN " + sqlIdent(b) + " ON ... -- table-level relationship: join columns not specified");
|
|
485
|
+
});
|
|
486
|
+
return lines.join("\n");
|
|
487
|
+
}
|
|
488
|
+
|
|
380
489
|
root.qdbd = {
|
|
490
|
+
findPath: findPath,
|
|
491
|
+
pathSql: pathSql,
|
|
381
492
|
renderDrawio: renderDrawio,
|
|
493
|
+
regions: regions,
|
|
382
494
|
select: select,
|
|
383
495
|
subset: subset,
|
|
384
496
|
colors: C,
|
|
@@ -394,5 +506,7 @@
|
|
|
394
506
|
root.qdbdRenderSvg = renderSvg;
|
|
395
507
|
root.qdbdSelect = select;
|
|
396
508
|
root.qdbdRenderDrawio = renderDrawio;
|
|
509
|
+
root.qdbdFindPath = findPath;
|
|
510
|
+
root.qdbdPathSql = pathSql;
|
|
397
511
|
root.qdbdNeighbourhood = neighbourhood;
|
|
398
512
|
})(typeof window !== "undefined" ? window : globalThis);
|
|
@@ -22,14 +22,15 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
|
|
|
22
22
|
.r.hl .rb { fill: #dbe6f6; fill-opacity: 1; }
|
|
23
23
|
.r:hover .rb { fill: #eef3fa; fill-opacity: 1; }
|
|
24
24
|
.t.flash .tb { stroke: #f0a020; stroke-width: 3; }
|
|
25
|
-
.t { cursor:
|
|
26
|
-
.
|
|
25
|
+
.t { cursor: grab; }
|
|
26
|
+
#canvas.panning .t { cursor: grabbing; }
|
|
27
27
|
|
|
28
28
|
/* toolbar (top right, like QuickDBD's zoom controls) */
|
|
29
29
|
.bar { position: absolute; top: 14px; right: 16px; display: flex; gap: 6px; align-items: center; }
|
|
30
30
|
.bar button, .seg button { border: 0; background: transparent; color: #555; font: inherit; cursor: pointer;
|
|
31
31
|
padding: 6px 9px; border-radius: 4px; }
|
|
32
32
|
.bar button:hover, .seg button:hover { background: #e4e7ea; }
|
|
33
|
+
.bar button.on { background: #fff; color: var(--blue); box-shadow: 0 1px 2px rgba(0,0,0,.12); }
|
|
33
34
|
.zoomval { min-width: 48px; text-align: center; color: #666; font-variant-numeric: tabular-nums; }
|
|
34
35
|
.seg { display: flex; background: #e9ecee; border-radius: 6px; padding: 2px; }
|
|
35
36
|
.seg button.on { background: #fff; color: var(--blue); box-shadow: 0 1px 2px rgba(0,0,0,.12); }
|
|
@@ -110,6 +111,20 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
|
|
|
110
111
|
padding: 8px 14px; border-radius: 6px; font-size: 12px; opacity: 0; transition: opacity .2s; pointer-events: none; z-index: 6; }
|
|
111
112
|
.toast.open { opacity: 1; }
|
|
112
113
|
|
|
114
|
+
/* path finder (in the details panel) */
|
|
115
|
+
.details .pathbox { display: flex; gap: 6px; margin: 10px 0 4px; }
|
|
116
|
+
.details .pathbox input { flex: 1; min-width: 0; padding: 5px 8px; border: 1px solid var(--line); border-radius: 5px; font: inherit; outline: none; }
|
|
117
|
+
.details .pathbox input:focus { border-color: var(--blue); }
|
|
118
|
+
.details .pathres ol { margin: 6px 0 8px 18px; padding: 0; }
|
|
119
|
+
.details .pathres li { border: 0; padding: 2px 0; }
|
|
120
|
+
.details .pathres pre { background: #f6f8fa; border: 1px solid #eceff2; border-radius: 6px; padding: 8px; font-size: 11px;
|
|
121
|
+
overflow-x: auto; margin: 6px 0; }
|
|
122
|
+
.details .pathres .acts a { margin-right: 12px; font-size: 12px; }
|
|
123
|
+
.details .pathres .none { margin-top: 6px; }
|
|
124
|
+
.e.ph .ep { stroke: #f0a020; stroke-width: 3; }
|
|
125
|
+
.e.ph path, .e.ph circle { stroke: #f0a020; }
|
|
126
|
+
.t.ph .tb { stroke: #f0a020; stroke-width: 2.5; }
|
|
127
|
+
|
|
113
128
|
/* diff banner (build --diff-base) */
|
|
114
129
|
.diffbar { position: absolute; top: 58px; left: 50%; transform: translateX(-50%); background: #fff; border: 1px solid var(--line);
|
|
115
130
|
border-radius: 8px; padding: 6px 12px; font-size: 12px; box-shadow: 0 4px 14px rgba(0,0,0,.06); display: none; z-index: 2;
|
|
@@ -165,3 +180,13 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
|
|
|
165
180
|
.CodeMirror-cursor { border-left-color: #fff !important; }
|
|
166
181
|
.CodeMirror-selected { background: #4a525a !important; }
|
|
167
182
|
.lineerr { background: rgba(229,115,115,.18); }
|
|
183
|
+
|
|
184
|
+
/* narrow screens and embedded frames: toolbar to the bottom, smaller minimap */
|
|
185
|
+
@media (max-width: 900px) {
|
|
186
|
+
.bar { top: auto; bottom: 12px; right: auto; left: 12px; flex-wrap: wrap; max-width: calc(100% - 24px);
|
|
187
|
+
background: rgba(255,255,255,.92); border-radius: 8px; padding: 2px; box-shadow: 0 2px 10px rgba(0,0,0,.08); }
|
|
188
|
+
.hint { display: none; }
|
|
189
|
+
.minimap { width: 140px; height: 100px; bottom: auto; top: 14px; right: 12px; }
|
|
190
|
+
.explorer { max-width: calc(100% - 180px); }
|
|
191
|
+
.details { left: 12px; right: 12px; width: auto; top: 12px; bottom: 64px; }
|
|
192
|
+
}
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
filter: "",
|
|
15
15
|
vm: data.model, // view model: the tables/relationships left after the filter
|
|
16
16
|
shown: {},
|
|
17
|
+
regions: !!data.model.regions,
|
|
18
|
+
path: null, // {to: id, steps: [...]} from the selected table
|
|
17
19
|
};
|
|
18
20
|
var svg = document.getElementById("canvas");
|
|
19
21
|
var vp = document.getElementById("vp");
|
|
@@ -27,7 +29,7 @@
|
|
|
27
29
|
|
|
28
30
|
/* ------------------------------------------------------------- render */
|
|
29
31
|
function render() {
|
|
30
|
-
vp.innerHTML = Q.renderInner(S.vm, S.pos, S.mode, { docMarks: true });
|
|
32
|
+
vp.innerHTML = Q.renderInner(S.vm, S.pos, S.mode, { docMarks: true, regions: S.regions });
|
|
31
33
|
applyFocus();
|
|
32
34
|
drawMinimap();
|
|
33
35
|
}
|
|
@@ -51,11 +53,15 @@
|
|
|
51
53
|
return [r.width || 800, r.height || 600];
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
function fit() {
|
|
55
|
-
var b = Q.bounds(S.vm, S.pos, S.mode), sz = stageSize(), pad = 40;
|
|
56
|
-
|
|
56
|
+
function fit(only) {
|
|
57
|
+
var b = Q.bounds(only ? Q.subset(S.vm, only) : S.vm, S.pos, S.mode), sz = stageSize(), pad = 40;
|
|
58
|
+
if (S.regions && !only) b = [b[0] - 30, b[1] - 60, b[2] + 30, b[3] + 30]; // room for region borders and labels
|
|
59
|
+
if (only) sz = [sz[0] - (details.classList.contains("open") ? 400 : 0), sz[1]]; // keep clear of the details panel
|
|
60
|
+
// Keep clear of the floating search box / toolbar at the top and, on narrow screens, the toolbar at the bottom.
|
|
61
|
+
var top = 70, bottom = sz[0] < 900 ? 70 : pad, availH = sz[1] - top - bottom;
|
|
62
|
+
var k = Math.min((sz[0] - 2 * pad) / (b[2] - b[0]), availH / (b[3] - b[1]), 1.5);
|
|
57
63
|
k = Math.max(k, 0.05);
|
|
58
|
-
S.view = { k: k, x: (sz[0] - (b[2] - b[0]) * k) / 2 - b[0] * k, y: (
|
|
64
|
+
S.view = { k: k, x: (sz[0] - (b[2] - b[0]) * k) / 2 - b[0] * k, y: top + (availH - (b[3] - b[1]) * k) / 2 - b[1] * k };
|
|
59
65
|
applyView();
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -94,6 +100,28 @@
|
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
function applyFocus() {
|
|
103
|
+
vp.querySelectorAll(".ph").forEach(function (el) { el.classList.remove("ph"); });
|
|
104
|
+
if (S.path && S.focus && S.path.steps) {
|
|
105
|
+
var on = {}, edges = [];
|
|
106
|
+
on[S.focus] = true;
|
|
107
|
+
S.path.steps.forEach(function (st) { on[st.to.t] = true; edges.push(st); });
|
|
108
|
+
svg.classList.add("focus");
|
|
109
|
+
vp.querySelectorAll(".t").forEach(function (el) {
|
|
110
|
+
var id = el.getAttribute("data-id");
|
|
111
|
+
el.classList.toggle("dim", !on[id]);
|
|
112
|
+
el.classList.toggle("ph", !!on[id]);
|
|
113
|
+
});
|
|
114
|
+
vp.querySelectorAll(".e").forEach(function (el) {
|
|
115
|
+
var a = el.getAttribute("data-a"), ac = el.getAttribute("data-ac"), b = el.getAttribute("data-b"), bc = el.getAttribute("data-bc");
|
|
116
|
+
var hit = edges.some(function (st) {
|
|
117
|
+
return (st.from.t === a && st.from.c === ac && st.to.t === b && st.to.c === bc) ||
|
|
118
|
+
(st.from.t === b && st.from.c === bc && st.to.t === a && st.to.c === ac);
|
|
119
|
+
});
|
|
120
|
+
el.classList.toggle("dim", !hit);
|
|
121
|
+
el.classList.toggle("ph", hit);
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
97
125
|
if (!S.focus || !byId[S.focus]) {
|
|
98
126
|
S.focus = null;
|
|
99
127
|
svg.classList.remove("focus");
|
|
@@ -180,12 +208,13 @@
|
|
|
180
208
|
if (ev.button !== 0) return;
|
|
181
209
|
var tEl = ev.target.closest && ev.target.closest(".t");
|
|
182
210
|
drag = { sx: ev.clientX, sy: ev.clientY, moved: false, table: tEl ? tEl.getAttribute("data-id") : null, el: tEl };
|
|
183
|
-
if (drag.table
|
|
211
|
+
if (drag.table) { // tables are draggable everywhere; only `qdbd serve` saves the new position
|
|
184
212
|
drag.start = S.pos[drag.table].slice();
|
|
213
|
+
tEl.parentNode.appendChild(tEl); // bring the grabbed table to the front
|
|
185
214
|
} else {
|
|
186
215
|
drag.view = { x: S.view.x, y: S.view.y };
|
|
187
|
-
svg.classList.add("panning");
|
|
188
216
|
}
|
|
217
|
+
svg.classList.add("panning");
|
|
189
218
|
svg.setPointerCapture(ev.pointerId);
|
|
190
219
|
});
|
|
191
220
|
var raf = null;
|
|
@@ -215,6 +244,7 @@
|
|
|
215
244
|
return;
|
|
216
245
|
}
|
|
217
246
|
if (d.start) {
|
|
247
|
+
if (S.regions) render(); // regions are sized from table positions
|
|
218
248
|
drawMinimap();
|
|
219
249
|
var name = byId[d.table].name, body = {};
|
|
220
250
|
body[name] = S.pos[d.table];
|
|
@@ -235,13 +265,28 @@
|
|
|
235
265
|
function center() { var s = stageSize(); return [s[0] / 2, s[1] / 2]; }
|
|
236
266
|
document.getElementById("zin").onclick = function () { var c = center(); zoomAt(1.25, c[0], c[1]); };
|
|
237
267
|
document.getElementById("zout").onclick = function () { var c = center(); zoomAt(0.8, c[0], c[1]); };
|
|
238
|
-
document.getElementById("zfit").onclick = fit;
|
|
268
|
+
document.getElementById("zfit").onclick = function () { fit(); };
|
|
239
269
|
document.querySelectorAll("#modes button").forEach(function (b) {
|
|
240
270
|
b.onclick = function () {
|
|
241
271
|
setMode(b.getAttribute("data-mode"));
|
|
242
272
|
};
|
|
243
273
|
});
|
|
244
274
|
|
|
275
|
+
var regionBtn = document.getElementById("xregions");
|
|
276
|
+
function groupCount() {
|
|
277
|
+
var g = {};
|
|
278
|
+
S.model.tables.forEach(function (t) { if (t.group) g[t.group] = true; });
|
|
279
|
+
return Object.keys(g).length;
|
|
280
|
+
}
|
|
281
|
+
function setRegions(on) {
|
|
282
|
+
S.regions = !!on && groupCount() > 0;
|
|
283
|
+
regionBtn.classList.toggle("on", S.regions);
|
|
284
|
+
regionBtn.style.display = groupCount() > 1 ? "" : "none";
|
|
285
|
+
render();
|
|
286
|
+
scheduleHash();
|
|
287
|
+
}
|
|
288
|
+
regionBtn.onclick = function () { setRegions(!S.regions); };
|
|
289
|
+
|
|
245
290
|
function setMode(mode) {
|
|
246
291
|
S.mode = ["full", "keys", "header"].indexOf(mode) >= 0 ? mode : "full";
|
|
247
292
|
document.querySelectorAll("#modes button").forEach(function (o) { o.classList.toggle("on", o.getAttribute("data-mode") === S.mode); });
|
|
@@ -251,6 +296,7 @@
|
|
|
251
296
|
|
|
252
297
|
/* ------------------------------------------------------------- selection: focus + details */
|
|
253
298
|
function select(id, opts) {
|
|
299
|
+
if ((id || null) !== S.focus) S.path = null;
|
|
254
300
|
S.focus = id && byId[id] ? id : null;
|
|
255
301
|
scheduleHash();
|
|
256
302
|
applyFocus();
|
|
@@ -431,6 +477,10 @@
|
|
|
431
477
|
if (t.file) meta.push(Q.esc(t.file) + ":" + t.line);
|
|
432
478
|
if (meta.length) out.push('<div class="meta">' + meta.join(" · ") + "</div>");
|
|
433
479
|
if (t.doc) out.push('<div class="doc">' + Q.esc(t.doc) + "</div>");
|
|
480
|
+
out.push('<div class="pathbox"><input id="pathto" list="pathlist" placeholder="Path to another table…" autocomplete="off" value="' +
|
|
481
|
+
(S.path && byId[S.path.to] ? Q.esc(byId[S.path.to].name) : "") + '"></div><datalist id="pathlist">' +
|
|
482
|
+
S.vm.tables.filter(function (x) { return x.id !== id; }).map(function (x) { return '<option value="' + Q.esc(x.name) + '">'; }).join("") +
|
|
483
|
+
'</datalist><div class="pathres" id="pathres">' + pathHtml(id) + "</div>");
|
|
434
484
|
if (t.diff && t.diff.state === "changed") {
|
|
435
485
|
var base = (S.model.diff && S.model.diff.base) || "base", ch = [];
|
|
436
486
|
t.cols.forEach(function (c) {
|
|
@@ -460,6 +510,47 @@
|
|
|
460
510
|
details.classList.add("open");
|
|
461
511
|
document.getElementById("stage").classList.add("has-details");
|
|
462
512
|
}
|
|
513
|
+
/* ------------------------------------------------------------- path finder */
|
|
514
|
+
function pathHtml(id) {
|
|
515
|
+
if (!S.path) return "";
|
|
516
|
+
var to = byId[S.path.to];
|
|
517
|
+
if (!S.path.steps) return '<div class="none">No relationship path from ' + Q.esc(byId[id].name) + " to " + Q.esc(to.name) + " (with the current filter).</div>";
|
|
518
|
+
var items = S.path.steps.map(function (st) {
|
|
519
|
+
return "<li>" + link(st.from.t, st.from.c) + " → " + link(st.to.t, st.to.c) +
|
|
520
|
+
'<span class="card">' + CARD[st.from.card] + ":" + CARD[st.to.card] + "</span></li>";
|
|
521
|
+
}).join("");
|
|
522
|
+
return "<h3>Path to " + Q.esc(to.name) + " (" + S.path.steps.length + " join" + (S.path.steps.length === 1 ? "" : "s") + ")</h3><ol>" + items +
|
|
523
|
+
"</ol><pre>" + Q.esc(Q.pathSql(S.vm, id, S.path.steps)) + '</pre><div class="acts"><a id="pathsql">Copy SQL</a><a id="pathonly">Show only this path</a><a id="pathclear">Clear</a></div>';
|
|
524
|
+
}
|
|
525
|
+
function findPathTo(name) {
|
|
526
|
+
var target = null;
|
|
527
|
+
S.vm.tables.forEach(function (t) { if (t.name.toLowerCase() === String(name).trim().toLowerCase()) target = t.id; });
|
|
528
|
+
if (!S.focus || !target || target === S.focus) return false;
|
|
529
|
+
S.path = { to: target, steps: Q.findPath(S.vm, S.focus, target) };
|
|
530
|
+
showDetails(S.focus);
|
|
531
|
+
applyFocus();
|
|
532
|
+
if (S.path.steps && !applyingHash) fit([S.focus].concat(S.path.steps.map(function (st) { return st.to.t; })));
|
|
533
|
+
scheduleHash();
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
dbody.addEventListener("keydown", function (ev) {
|
|
537
|
+
if (ev.target.id === "pathto" && ev.key === "Enter") findPathTo(ev.target.value);
|
|
538
|
+
if (ev.target.id === "pathto" && ev.key === "Escape") ev.stopPropagation();
|
|
539
|
+
});
|
|
540
|
+
dbody.addEventListener("change", function (ev) { if (ev.target.id === "pathto") findPathTo(ev.target.value); });
|
|
541
|
+
dbody.addEventListener("click", function (ev) {
|
|
542
|
+
if (ev.target.id === "pathclear") { S.path = null; showDetails(S.focus); applyFocus(); scheduleHash(); }
|
|
543
|
+
else if (ev.target.id === "pathonly" && S.path && S.path.steps) {
|
|
544
|
+
var names = [byId[S.focus].name].concat(S.path.steps.map(function (st) { return byId[st.to.t].name; }));
|
|
545
|
+
// Selector syntax treats spaces, commas, + and a leading ! specially; match those characters with "?".
|
|
546
|
+
applyFilter(names.map(function (n) { return n.replace(/[\s,+!]/g, "?"); }).join(" "));
|
|
547
|
+
} else if (ev.target.id === "pathsql" && S.path && S.path.steps) {
|
|
548
|
+
var sql = Q.pathSql(S.vm, S.focus, S.path.steps);
|
|
549
|
+
if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(sql).then(function () { showToast("SQL copied"); });
|
|
550
|
+
else window.prompt("Copy SQL:", sql);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
463
554
|
function closeDetails() {
|
|
464
555
|
details.classList.remove("open");
|
|
465
556
|
document.getElementById("stage").classList.remove("has-details");
|
|
@@ -519,14 +610,14 @@
|
|
|
519
610
|
}
|
|
520
611
|
function exportSvg() {
|
|
521
612
|
var only = S.focus ? Object.keys(neighbours(S.focus)) : null;
|
|
522
|
-
return Q.renderSvg(S.vm, S.pos, { mode: S.mode, only: only, fontCss: document.getElementById("qdbd-font").textContent });
|
|
613
|
+
return Q.renderSvg(S.vm, S.pos, { mode: S.mode, only: only, regions: S.regions, fontCss: document.getElementById("qdbd-font").textContent });
|
|
523
614
|
}
|
|
524
615
|
document.getElementById("xsvg").onclick = function () {
|
|
525
616
|
download((data.title || "diagram") + ".svg", new Blob([exportSvg()], { type: "image/svg+xml" }));
|
|
526
617
|
};
|
|
527
618
|
document.getElementById("xdrawio").onclick = function () {
|
|
528
619
|
var only = S.focus ? Object.keys(neighbours(S.focus)) : null;
|
|
529
|
-
var xml = Q.renderDrawio(S.vm, S.pos, { mode: S.mode, only: only, title: data.title || "ERD" });
|
|
620
|
+
var xml = Q.renderDrawio(S.vm, S.pos, { mode: S.mode, only: only, title: data.title || "ERD", regions: S.regions });
|
|
530
621
|
download((data.title || "diagram") + ".drawio", new Blob([xml], { type: "application/xml" }));
|
|
531
622
|
};
|
|
532
623
|
document.getElementById("xpng").onclick = function () {
|
|
@@ -564,6 +655,9 @@
|
|
|
564
655
|
|
|
565
656
|
index();
|
|
566
657
|
if (S.serve) document.body.classList.add("serve");
|
|
658
|
+
if (stageSize()[0] < 900) explorer.classList.add("collapsed"); // small screens / embedded frames
|
|
659
|
+
regionBtn.classList.toggle("on", S.regions);
|
|
660
|
+
regionBtn.style.display = groupCount() > 1 ? "" : "none";
|
|
567
661
|
computeView();
|
|
568
662
|
render();
|
|
569
663
|
drawTree();
|
|
@@ -597,7 +691,9 @@
|
|
|
597
691
|
var p = new URLSearchParams(), v = S.view, sz = stageSize();
|
|
598
692
|
if (S.filter) p.set("f", S.filter);
|
|
599
693
|
if (S.focus && byId[S.focus]) p.set("t", byId[S.focus].name);
|
|
694
|
+
if (S.path && byId[S.path.to]) p.set("p", byId[S.path.to].name);
|
|
600
695
|
if (S.mode !== "full") p.set("m", S.mode);
|
|
696
|
+
if (S.regions !== !!S.model.regions) p.set("r", S.regions ? "1" : "0");
|
|
601
697
|
p.set("z", String(Math.round(v.k * 1000) / 1000));
|
|
602
698
|
p.set("c", Math.round((sz[0] / 2 - v.x) / v.k) + "," + Math.round((sz[1] / 2 - v.y) / v.k));
|
|
603
699
|
return "#" + p.toString();
|
|
@@ -615,10 +711,12 @@
|
|
|
615
711
|
applyingHash = true;
|
|
616
712
|
try {
|
|
617
713
|
if (p.has("m")) setMode(p.get("m"));
|
|
714
|
+
if (p.has("r")) setRegions(p.get("r") === "1");
|
|
618
715
|
applyFilter(p.get("f") || "", true);
|
|
619
716
|
var name = p.get("t"), id = null;
|
|
620
717
|
if (name) S.model.tables.forEach(function (t) { if (t.name === name) id = t.id; });
|
|
621
718
|
select(id && S.shown[id] ? id : null);
|
|
719
|
+
if (p.get("p") && S.focus) findPathTo(p.get("p"));
|
|
622
720
|
var z = parseFloat(p.get("z")), c = (p.get("c") || "").split(",").map(Number);
|
|
623
721
|
if (z > 0 && c.length === 2 && !isNaN(c[0]) && !isNaN(c[1])) {
|
|
624
722
|
var sz = stageSize();
|