ripple-sql 0.1.0__tar.gz → 0.1.2__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.
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/CHANGELOG.md +16 -1
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/PKG-INFO +1 -1
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/pyproject.toml +1 -1
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/cli.py +10 -3
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/home.py +6 -1
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/loaders/dbt.py +5 -2
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/loaders/dbt_config.py +3 -1
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/loaders/sqldir.py +2 -2
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/loaders/types.py +30 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/lookml.py +2 -1
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/project.py +1 -1
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/semantic.py +2 -1
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/static/answer.css +2 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/static/answer.html +13 -3
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/static/explore.js +5 -2
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/.gitignore +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/LICENSE +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/README.md +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/__init__.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/answer.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/answer_page.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/cache.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/ci.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/ci_signature.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/doctor.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/__init__.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/budget.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/column_lineage.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/column_ref.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/cte_tracing.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/dependencies.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/dialect.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/dispatch.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/extraction.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/jinja.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/json_sources.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/macro_source.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/pipeline.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/preprocess.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/safe_gen.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/schema_qualification.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/scope.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/select_sources.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/sql_script.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/statement.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/tech_debt.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/tsql_catalog.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/tsql_scalar_vars.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/tsql_tvf.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/tsql_xml.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/types.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/unused_deps.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/validation.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/graph.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/loaders/__init__.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/loaders/identity.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/loaders/sidecar.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/mcp_server.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/names.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/py.typed +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/render.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/render_shims.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/schemas.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/server.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/sourcefiles.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/star_resolution.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/static/answer_twin.js +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/usage/__init__.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/usage/cli.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/usage/collect.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/usage/discover.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/usage/ingest.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/usage/report.py +0 -0
|
@@ -6,6 +6,19 @@ Notable changes to Ripple. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.2] - 2026-09-09
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- A symlink whose target is gone matched the SQL, YAML, LookML, macro, and seed globs and crashed the load with a traceback on the first real folder the published build ran in. One helper, `files_under`, now serves every project glob and keeps regular files only.
|
|
13
|
+
|
|
14
|
+
## [0.1.1] - 2026-09-09
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- The map's home page says what it is for in one line ("Pick a column. See what breaks if it changes, or where it comes from.") and offers one example column to try; the five most-read columns come back with an answer. A source table's chip no longer shows a count, since the number was the columns some model happened to name, not the table; opening one says the list is not declared in the project and points at the Coverage rung that adds it.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Run from a home directory or the root of a disk, ripple refuses in one sentence instead of reading every SQL file the user owns. Progress lines ("scanning ...") reach a pipe as they are printed, not when the process exits.
|
|
21
|
+
|
|
9
22
|
## [0.1.0] - 2026-09-08
|
|
10
23
|
|
|
11
24
|
First public release.
|
|
@@ -222,5 +235,7 @@ First public release.
|
|
|
222
235
|
### Removed
|
|
223
236
|
- The dark whole-graph canvas that `ripple serve` used to open (`static/index.html`). Every surface now draws one answer.
|
|
224
237
|
|
|
225
|
-
[Unreleased]: https://github.com/bteh/ripple/compare/v0.1.
|
|
238
|
+
[Unreleased]: https://github.com/bteh/ripple/compare/v0.1.2...HEAD
|
|
239
|
+
[0.1.2]: https://github.com/bteh/ripple/releases/tag/v0.1.2
|
|
240
|
+
[0.1.1]: https://github.com/bteh/ripple/releases/tag/v0.1.1
|
|
226
241
|
[0.1.0]: https://github.com/bteh/ripple/releases/tag/v0.1.0
|
|
@@ -570,10 +570,12 @@ def utf8_streams() -> None:
|
|
|
570
570
|
for stream in (sys.stdin, sys.stdout, sys.stderr):
|
|
571
571
|
if stream is None or not hasattr(stream, "reconfigure"):
|
|
572
572
|
continue
|
|
573
|
+
# line buffered either way: "scanning ..." must show while the scan
|
|
574
|
+
# runs, not when the process exits, even through a pipe
|
|
573
575
|
if stream.isatty():
|
|
574
|
-
stream.reconfigure(errors="replace")
|
|
576
|
+
stream.reconfigure(errors="replace", line_buffering=True)
|
|
575
577
|
else:
|
|
576
|
-
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
578
|
+
stream.reconfigure(encoding="utf-8", errors="replace", line_buffering=True)
|
|
577
579
|
|
|
578
580
|
|
|
579
581
|
def main(argv: list[str] | None = None) -> None:
|
|
@@ -726,7 +728,12 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
726
728
|
"doctor": cmd_doctor,
|
|
727
729
|
"serve": cmd_serve,
|
|
728
730
|
}
|
|
729
|
-
|
|
731
|
+
from ripple.loaders.types import NotAProject
|
|
732
|
+
|
|
733
|
+
try:
|
|
734
|
+
handlers[args.command](args)
|
|
735
|
+
except NotAProject as e:
|
|
736
|
+
sys.exit(str(e))
|
|
730
737
|
|
|
731
738
|
|
|
732
739
|
if __name__ == "__main__":
|
|
@@ -130,10 +130,14 @@ def model_map(nodes: list[dict], edges) -> dict:
|
|
|
130
130
|
members: dict[str, dict] = {}
|
|
131
131
|
for n in nodes:
|
|
132
132
|
source = n["type"] == "source"
|
|
133
|
+
columns = [c for c in n.get("columns") or [] if _is_column(c)]
|
|
133
134
|
members[n["name"]] = {
|
|
134
135
|
"id": n["name"],
|
|
135
136
|
"kind": "source" if source else "model",
|
|
136
|
-
"columns":
|
|
137
|
+
"columns": columns,
|
|
138
|
+
# a source with no declared list only shows the columns some
|
|
139
|
+
# model happens to name; the page must not present that as the table
|
|
140
|
+
"declared": bool(columns),
|
|
137
141
|
"status": None if source else n.get("status"),
|
|
138
142
|
"path": None if source else n.get("path"),
|
|
139
143
|
}
|
|
@@ -156,6 +160,7 @@ def model_map(nodes: list[dict], edges) -> dict:
|
|
|
156
160
|
"id": name,
|
|
157
161
|
"kind": kind,
|
|
158
162
|
"columns": sorted(seen.get(name, {})),
|
|
163
|
+
"declared": kind == "dashboard",
|
|
159
164
|
"status": None,
|
|
160
165
|
"path": None,
|
|
161
166
|
}
|
|
@@ -24,6 +24,7 @@ from ripple.loaders.types import (
|
|
|
24
24
|
DEFAULT_DIALECT,
|
|
25
25
|
Model,
|
|
26
26
|
Project,
|
|
27
|
+
files_under,
|
|
27
28
|
relative_posix,
|
|
28
29
|
)
|
|
29
30
|
|
|
@@ -124,7 +125,7 @@ def _seed_tables(root: Path) -> list[Model]:
|
|
|
124
125
|
|
|
125
126
|
seeds: list[Model] = []
|
|
126
127
|
for seed_dir in _seed_dirs(root):
|
|
127
|
-
for path in
|
|
128
|
+
for path in files_under(seed_dir, "*.csv"):
|
|
128
129
|
try:
|
|
129
130
|
with open(path, newline="", encoding="utf-8", errors="replace") as f:
|
|
130
131
|
header = next(csv.reader(f), [])
|
|
@@ -257,7 +258,9 @@ def _load_dbt_raw(root: Path, dialect: str | None) -> Project:
|
|
|
257
258
|
"run 'dbt compile' for exact lineage."
|
|
258
259
|
]
|
|
259
260
|
model_dirs = _dbt_model_dirs(root)
|
|
260
|
-
pairs = sorted(
|
|
261
|
+
pairs = sorted(
|
|
262
|
+
((d, p) for d in model_dirs for p in files_under(d, "*.sql")), key=lambda t: t[1]
|
|
263
|
+
)
|
|
261
264
|
# a slice of every model, the same sample the sql-dir loader sniffs
|
|
262
265
|
sample = "\n".join(p.read_text(errors="replace", encoding="utf-8")[:4000] for _, p in pairs)[
|
|
263
266
|
:200000
|
|
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
import re
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
from ripple.loaders.types import files_under
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
def _project_vars(root: Path) -> dict:
|
|
10
12
|
"""Declared vars from dbt_project.yml. A sub-dict scoped under the
|
|
@@ -236,7 +238,7 @@ def _macro_sources(root: Path) -> list[tuple[str | None, str]]:
|
|
|
236
238
|
macros_dir = base / dir_name
|
|
237
239
|
if not macros_dir.is_dir():
|
|
238
240
|
continue
|
|
239
|
-
for macro_file in
|
|
241
|
+
for macro_file in files_under(macros_dir, "*.sql"):
|
|
240
242
|
resolved = macro_file.resolve()
|
|
241
243
|
if resolved in seen_files:
|
|
242
244
|
continue
|
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
from ripple.engine.budget import TIMED_OUT, budget_seconds, max_sql_bytes, run_within_budget
|
|
8
8
|
from ripple.loaders.identity import directory_identity
|
|
9
|
-
from ripple.loaders.types import IGNORE_DIRS, Model, Project, relative_posix
|
|
9
|
+
from ripple.loaders.types import IGNORE_DIRS, Model, Project, files_under, relative_posix
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def _iter_sql_files(root: Path, exclude: list[Path] | None = None) -> tuple[list[Path], int]:
|
|
@@ -19,7 +19,7 @@ def _iter_sql_files(root: Path, exclude: list[Path] | None = None) -> tuple[list
|
|
|
19
19
|
kept: list[Path] = []
|
|
20
20
|
skipped = 0
|
|
21
21
|
excluded = [d.resolve() for d in (exclude or [])]
|
|
22
|
-
for path in
|
|
22
|
+
for path in files_under(root, "*.sql"):
|
|
23
23
|
dirs = path.relative_to(root).parts[:-1]
|
|
24
24
|
if any(part in IGNORE_DIRS or part.startswith(".") for part in dirs):
|
|
25
25
|
skipped += 1
|
|
@@ -169,6 +169,16 @@ def find_project_root(start: Path) -> Path:
|
|
|
169
169
|
for candidate in [current, *current.parents]:
|
|
170
170
|
if (candidate / "dbt_project.yml").exists():
|
|
171
171
|
return candidate
|
|
172
|
+
# every caller passes through here before it stamps the cache or reads
|
|
173
|
+
# a file, so this is where a whole machine is refused
|
|
174
|
+
if whole_machine(current):
|
|
175
|
+
where = (
|
|
176
|
+
"your home directory" if current == Path.home().resolve() else "the root of the disk"
|
|
177
|
+
)
|
|
178
|
+
raise NotAProject(
|
|
179
|
+
f"{current} is {where}, not a project; run ripple inside a project folder, "
|
|
180
|
+
"or pass the folder with --path"
|
|
181
|
+
)
|
|
172
182
|
return current
|
|
173
183
|
|
|
174
184
|
|
|
@@ -195,3 +205,23 @@ def relative_posix(path, root) -> str:
|
|
|
195
205
|
OS, so a model's path is the same string in a PR comment written on
|
|
196
206
|
Linux and on the laptop that reads it."""
|
|
197
207
|
return path.relative_to(root).as_posix()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class NotAProject(ValueError):
|
|
211
|
+
"""The path is a whole machine, not a project: reading it would crawl
|
|
212
|
+
everything under the home directory or the disk."""
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def whole_machine(path: Path) -> bool:
|
|
216
|
+
"""True for the home directory and for the root of a disk. A plain-SQL
|
|
217
|
+
scan starts wherever the command runs, and from either of those it
|
|
218
|
+
would read every file the user owns."""
|
|
219
|
+
here = Path(path).resolve()
|
|
220
|
+
return here == Path.home().resolve() or here == Path(here.anchor)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def files_under(root: Path, pattern: str) -> list[Path]:
|
|
224
|
+
"""The regular files matching a glob under root, sorted. A symlink whose
|
|
225
|
+
target is gone matches the glob and is not a file; reading it raised a
|
|
226
|
+
traceback on the first real folder this ran in."""
|
|
227
|
+
return sorted(p for p in root.rglob(pattern) if p.is_file())
|
|
@@ -16,6 +16,7 @@ from pathlib import Path
|
|
|
16
16
|
|
|
17
17
|
import yaml
|
|
18
18
|
|
|
19
|
+
from ripple.loaders.types import files_under
|
|
19
20
|
from ripple.project import IGNORE_DIRS
|
|
20
21
|
from ripple.semantic import DownstreamEdge
|
|
21
22
|
|
|
@@ -133,7 +134,7 @@ def load_lookml_downstream(root: Path) -> list[DownstreamEdge]:
|
|
|
133
134
|
return []
|
|
134
135
|
|
|
135
136
|
edges: list[DownstreamEdge] = []
|
|
136
|
-
for path in
|
|
137
|
+
for path in files_under(root, "*.lkml"):
|
|
137
138
|
parts = path.relative_to(root).parts[:-1]
|
|
138
139
|
if any(p in IGNORE_DIRS or p.startswith(".") for p in parts):
|
|
139
140
|
continue
|
|
@@ -49,7 +49,7 @@ __all__ = [
|
|
|
49
49
|
|
|
50
50
|
def load_project(path: str | Path = ".", dialect: str | None = None) -> Project:
|
|
51
51
|
start = Path(path).resolve()
|
|
52
|
-
root = find_project_root(start)
|
|
52
|
+
root = find_project_root(start) # refuses a home directory or a disk root
|
|
53
53
|
# If we're standing above several dbt projects (a monorepo), gather them
|
|
54
54
|
# all. find_project_root only walks UP to the nearest one, so scan DOWN.
|
|
55
55
|
scan_root = root if (root / "dbt_project.yml").exists() else start
|
|
@@ -19,6 +19,7 @@ from pathlib import Path
|
|
|
19
19
|
|
|
20
20
|
import yaml
|
|
21
21
|
|
|
22
|
+
from ripple.loaders.types import files_under
|
|
22
23
|
from ripple.project import IGNORE_DIRS
|
|
23
24
|
|
|
24
25
|
_REF = re.compile(r"ref\(\s*['\"]([^'\"]+)['\"]\s*(?:,\s*['\"]([^'\"]+)['\"])?\s*\)")
|
|
@@ -47,7 +48,7 @@ def _ref_model(value) -> str | None:
|
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
def _iter_schema_docs(root: Path):
|
|
50
|
-
for path in
|
|
51
|
+
for path in files_under(root, "*.yml") + files_under(root, "*.yaml"):
|
|
51
52
|
parts = path.relative_to(root).parts[:-1]
|
|
52
53
|
if any(p in IGNORE_DIRS or p.startswith(".") for p in parts):
|
|
53
54
|
continue
|
|
@@ -24,6 +24,7 @@ h1 code{font:500 24px/1 var(--mono); letter-spacing:0}
|
|
|
24
24
|
.sum b{color:var(--ink); font-weight:600}
|
|
25
25
|
.sum > span{position:relative}
|
|
26
26
|
.sum > span + span::before{content:""; position:absolute; left:-9px; top:.3em; height:.9em; border-left:1px solid var(--line-strong)}
|
|
27
|
+
.lede{margin:14px 0 0; font-size:16px; color:var(--ink); text-wrap:pretty}
|
|
27
28
|
|
|
28
29
|
.ask{display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin:28px 0 0; padding:16px 0 14px; border-top:1px solid var(--line); border-bottom:1px solid var(--line)}
|
|
29
30
|
.ask input,.ask select{font:400 14px var(--mono); padding:8px 10px; border:1px solid var(--line-strong); border-radius:var(--r); background:var(--surface); color:var(--ink); min-width:0; height:36px}
|
|
@@ -105,6 +106,7 @@ pre{margin:0; padding:16px 22px 18px; overflow-x:auto; font:400 13px/1.65 var(--
|
|
|
105
106
|
.cols{transition:opacity .18s var(--ease)}
|
|
106
107
|
.wires path.on{stroke:var(--accent); stroke-width:1.5}
|
|
107
108
|
.cols{display:flex; flex-direction:column; gap:5px; margin:0 0 6px 10px; padding-left:10px; border-left:1px solid var(--line)}
|
|
109
|
+
.cols .gname{max-width:36ch; white-space:normal; line-height:1.45; padding-top:2px}
|
|
108
110
|
.cols .node{cursor:pointer}
|
|
109
111
|
.cols .node:hover{border-color:var(--accent); color:var(--accent-strong)}
|
|
110
112
|
.more-chips{font:500 12.5px var(--sans); color:var(--accent); background:none; border:0; padding:4px 0; cursor:pointer; text-align:left}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
<p class="eyebrow"><span class="mark" aria-hidden="true"></span><span id="eyebrow">Ripple</span></p>
|
|
14
14
|
<h1 id="title"></h1>
|
|
15
15
|
<p class="sum" id="sum"></p>
|
|
16
|
+
<p class="lede" id="lede" hidden>Pick a column. See what breaks if it changes, or where it comes from.</p>
|
|
16
17
|
|
|
17
18
|
<div class="change" id="change" hidden><span class="lab">In this change</span></div>
|
|
18
19
|
|
|
@@ -77,7 +78,8 @@
|
|
|
77
78
|
demo.hidden = false;
|
|
78
79
|
mapEl.hidden = true;
|
|
79
80
|
nav.hidden = !mapApi;
|
|
80
|
-
|
|
81
|
+
document.getElementById('lede').hidden = true;
|
|
82
|
+
if (mapApi) starterStrip(true);
|
|
81
83
|
document.getElementById('eyebrow').textContent = 'Ripple · ' + (DATA.change ? 'what this change breaks' : (a.kind === 'trace' ? 'where it comes from' : 'what breaks'));
|
|
82
84
|
document.getElementById('title').innerHTML = a.kind === 'trace'
|
|
83
85
|
? 'Where does <code>' + esc(a.target) + '</code> come from?'
|
|
@@ -185,6 +187,14 @@
|
|
|
185
187
|
|
|
186
188
|
// ---- the home state: ripple serve opens here, on the whole project ----
|
|
187
189
|
var P = DATA.project || {}, L = DATA.live;
|
|
190
|
+
// one example on the map ("Try"), the five most-read columns with an answer
|
|
191
|
+
function starterStrip(all){
|
|
192
|
+
var strip = document.getElementById('starters');
|
|
193
|
+
if (!L || !L.starters.length) return;
|
|
194
|
+
strip.hidden = false;
|
|
195
|
+
strip.querySelector('.lab').textContent = all ? 'Start with' : 'Try';
|
|
196
|
+
Array.prototype.slice.call(strip.querySelectorAll('button')).forEach(function(b, i){ b.hidden = !all && i > 0; });
|
|
197
|
+
}
|
|
188
198
|
function home(){
|
|
189
199
|
current = null;
|
|
190
200
|
demo.hidden = true;
|
|
@@ -200,9 +210,9 @@
|
|
|
200
210
|
if (L.review) bits.push('<b>' + L.review + ' need review</b>');
|
|
201
211
|
document.getElementById('sum').innerHTML = bits.map(function(b){ return '<span>' + b + '</span>'; }).join('');
|
|
202
212
|
document.title = 'Ripple: ' + (P.root || 'local');
|
|
213
|
+
document.getElementById('lede').hidden = !mapApi;
|
|
203
214
|
if (mapApi){
|
|
204
|
-
|
|
205
|
-
document.getElementById('starters').hidden = true;
|
|
215
|
+
starterStrip(false);
|
|
206
216
|
mapEl.hidden = false;
|
|
207
217
|
mapApi.filter(document.getElementById('q').value.trim());
|
|
208
218
|
requestAnimationFrame(mapApi.draw);
|
|
@@ -47,7 +47,8 @@ function renderMap(root, map, opts){
|
|
|
47
47
|
var broken = m.status && m.status !== 'ok' && m.status !== 'star_only';
|
|
48
48
|
var b = el('button', 'mdl' + (m.kind === 'source' ? ' src' : '') + (m.kind === 'dashboard' ? ' bi' : '') + (broken ? ' r' : ''));
|
|
49
49
|
b.type = 'button'; b.dataset.id = m.id; b.setAttribute('aria-expanded', 'false');
|
|
50
|
-
|
|
50
|
+
// a source's count would be the columns some model names, not the table's width
|
|
51
|
+
b.innerHTML = label(m) + (m.declared && m.kind !== 'source' && m.columns.length ? ' <small>' + m.columns.length + '</small>' : '');
|
|
51
52
|
var reads = (ups[m.id] || []).length, feeds = (downs[m.id] || []).length;
|
|
52
53
|
b.title = (m.path || m.id) + ' · reads ' + reads + ', feeds ' + feeds + (broken ? ' · ' + m.status.replace('_', ' ') : '');
|
|
53
54
|
b.addEventListener('click', function(){ toggle(m, g, b); });
|
|
@@ -81,7 +82,9 @@ function renderMap(root, map, opts){
|
|
|
81
82
|
});
|
|
82
83
|
var note = null;
|
|
83
84
|
if (m.columns.length > MAP_COLUMNS) note = '+ ' + (m.columns.length - MAP_COLUMNS) + ' more, ask by name';
|
|
84
|
-
else if (!m.
|
|
85
|
+
else if (!m.declared && m.kind === 'source') note = (m.columns.length ? 'only the columns this project names; the full list is' : 'no column list; it is') +
|
|
86
|
+
' not declared in the project (a select * reads it unnamed). Coverage below adds it.';
|
|
87
|
+
else if (!m.columns.length) note = 'no columns known';
|
|
85
88
|
if (note){ var n = el('span', 'gname'); n.textContent = note; cols.appendChild(n); }
|
|
86
89
|
g.appendChild(cols); b.setAttribute('aria-expanded', 'true'); draw();
|
|
87
90
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|