diff --git a/CHANGELOG.md b/CHANGELOG.md index 746bf63..7696102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed -- Setup failures now print the full error in red, and the agent hand-off prompt includes the error text so the agent can diagnose it directly. -- Error output with bracket tokens (e.g. `[notice]`) is no longer swallowed by rich markup. +- Setup failures print the full error in red and include it in the agent hand-off prompt, so the agent can diagnose it directly. +- Connecting the playground workspace no longer fails with a confusing "already exists" error; setup connects to it directly. ### Changed -- The launch-plan preview shows an `` placeholder instead of repeating the error; wrapped lines keep their indentation; non-interactive runs no longer hard-wrap the hand-off prompt. -- The hand-off prompt prints as plain flush-left text instead of inside a panel, so it can be selected and copied manually without grabbing box borders. -- The scaffold's `.scripts/show_notebook.py` opens the notebook with `?hide_header=true` for a cleaner view. -- Refreshed the bundled minimal workspace `uv.lock` (`dlthub-client` 0.28.1, `marimo` 0.23.13, `pandas` 3.0.3 — 3.0.4 was yanked for datetime segfaults) and updated the notebook session snapshot's pinned marimo version. +- The hand-off prompt prints as plain text (no panel), so it can be copied manually. +- The onboarding notebook is a single page; its "Next step" button navigates to the organization's setup page on dltHub. +- The dataset viewer now hides the notebook header, and opens on whichever stack (local or hosted) you're connected to instead of always pointing to production. +- Refreshed the bundled workspace `uv.lock` (`dlthub-client` 0.28.1, `marimo` 0.23.13; `pandas` 3.0.3 — 3.0.4 was yanked). ## [0.10.1] - 2026-07-01 diff --git a/Makefile b/Makefile index 3c38ef6..55ab3eb 100644 --- a/Makefile +++ b/Makefile @@ -127,14 +127,14 @@ workspace-env: ## Like workspace, but pins api_base_url (+ auth_base_url / dlthu fi; \ echo "workspace-env: pinned + validated api_base_url = $(API_BASE_URL)$(if $(AUTH_BASE_URL), (auth_base_url = $(AUTH_BASE_URL))) in $$cfg" -workspace-local: ## Scaffold a workspace pointed at the local stack (api + auth on *.dlthub.test) with an editable dlthub-client; skips TLS verify (mkcert CA is not in Python's bundle) - $(MAKE) workspace-env API_BASE_URL=https://api.dlthub.test AUTH_BASE_URL=https://auth.dlthub.test DLT_RUNTIME_INSECURE=true DLTHUB_CLIENT_SOURCE="$(or $(DLTHUB_CLIENT_SOURCE),$(CURDIR)/../runtime/clients/cli)" +workspace-local: ## Scaffold a workspace pointed at the local stack (api + auth on *.dlthub.test) with the released dlthub-client; skips TLS verify (mkcert CA is not in Python's bundle) + $(MAKE) workspace-env API_BASE_URL=https://api.dlthub.test AUTH_BASE_URL=https://auth.dlthub.test DLT_RUNTIME_INSECURE=true workspace-stage: ## Scaffold a workspace pointed at the staging stack (api.dlthub.net) $(MAKE) workspace-env API_BASE_URL=https://api.dlthub.net -workspace-dev: ## Scaffold a workspace pointed at the dev stack (api.dlthub.dev) with an editable dlthub-client matching the dev API - $(MAKE) workspace-env API_BASE_URL=https://api.dlthub.dev DLTHUB_CLIENT_SOURCE="$(or $(DLTHUB_CLIENT_SOURCE),$(CURDIR)/../runtime/clients/cli)" +workspace-dev: ## Scaffold a workspace pointed at the dev stack (api.dlthub.dev) with the released dlthub-client + $(MAKE) workspace-env API_BASE_URL=https://api.dlthub.dev workspace-here: dev ## Init in place: make empty ./$(WORKSPACE_HERE_DIR), cd in, run the local CLI with no positional (pass ARGS="--yes --skip-uv-sync") @case "$(WORKSPACE_HERE_DIR)" in *..*|"") echo "invalid WORKSPACE_HERE_DIR: $(WORKSPACE_HERE_DIR)"; exit 1;; esac diff --git a/src/create_dlthub_workspace/cli.py b/src/create_dlthub_workspace/cli.py index 5898de9..d3b5ce8 100644 --- a/src/create_dlthub_workspace/cli.py +++ b/src/create_dlthub_workspace/cli.py @@ -3,7 +3,6 @@ from __future__ import annotations import argparse -import re import shutil import subprocess import sys @@ -43,7 +42,7 @@ validate_agent, validate_scaffold_name, ) -from .uv import capture_uv_command, execute_uv_install, find_uv, run_uv_command, run_uv_sync +from .uv import execute_uv_install, find_uv, run_uv_command, run_uv_sync def _ensure_utf8_io_on_windows() -> None: @@ -325,55 +324,13 @@ def _login_and_connect_playground(uv_executable: str, project_dir: Path, *, verb """Log in and bind the playground workspace, the setup the entry skill assumes is done.""" with substep(strings.MSG_CONNECTING_DLTHUB, strings.MSG_CONNECTED_DLTHUB, verbose=verbose): run_uv_command(uv_executable, project_dir, ["run", "dlthub", "login"], verbose=verbose) - # connect --create errors on an existing workspace, so pass it only when absent. - connect_args = ["run", "dlthub", "workspace", "connect", PLAYGROUND_WORKSPACE] - if not _playground_exists(uv_executable, project_dir): - connect_args.append("--create") - run_uv_command(uv_executable, project_dir, connect_args, verbose=verbose) - - -def _workspace_in_list(list_output: str, name: str) -> bool: - """True if ``name`` appears in the Name column of `dlthub workspace list`. - - The output is a space-padded table; workspace names can contain single - spaces (e.g. "My Workspace"), so columns are split on runs of 2+ spaces and - the first field is the name. The header row (before the dashed separator) - and the separator itself are skipped, so a workspace literally named like a - column header can't false-match. - """ - seen_separator = False - for line in list_output.splitlines(): - stripped = line.strip() - if not stripped: - continue - if set(stripped) <= {"-", " "}: - seen_separator = True - continue - if not seen_separator: - continue # header row(s) above the separator - first_column = re.split(r"\s{2,}", stripped)[0] - if first_column == name: - return True - return False - - -def _playground_exists(uv_executable: str, project_dir: Path) -> bool: - """Report whether the playground workspace already exists for the user. - - Lists remote workspaces with --non-interactive so an unauthenticated user - fails fast (no hanging prompt) instead of blocking. On any failure we report - False, so the caller falls back to `connect --create` — and that connect - step then triggers the interactive login. - """ - try: - output = capture_uv_command( + # The account always has a playground workspace, so connect without --create. + run_uv_command( uv_executable, project_dir, - ["run", "dlthub", "--non-interactive", "workspace", "list"], + ["run", "dlthub", "workspace", "connect", PLAYGROUND_WORKSPACE], + verbose=verbose, ) - except UvError: - return False - return _workspace_in_list(output, PLAYGROUND_WORKSPACE) if __name__ == "__main__": diff --git a/src/create_dlthub_workspace/scaffolds/minimal_workspace/.scripts/show_notebook.py b/src/create_dlthub_workspace/scaffolds/minimal_workspace/.scripts/show_notebook.py index 6b157b9..8aa69f5 100644 --- a/src/create_dlthub_workspace/scaffolds/minimal_workspace/.scripts/show_notebook.py +++ b/src/create_dlthub_workspace/scaffolds/minimal_workspace/.scripts/show_notebook.py @@ -1,44 +1,34 @@ """Open a deployed notebook's read-only "show" page in the dltHub web app. Builds and opens: - {APP_BASE}/w/{workspace_id}/notebooks/{job_ref}/show?hide_header=true + {web_ui_base}/w/{workspace_id}/notebooks/{job_ref}/show?hide_header=true -The workspace id is read from this workspace's `.dlt/config.toml` (so it tracks -whatever this workspace is connected to). Pass the job ref as the only argument. -Override the web-app base with DLTHUB_APP_URL (defaults to prod). +The workspace id and web-app base come from the active workspace context — +`dlt_runtime.urls` mirrors the web app's routes — so the URL tracks whatever +stack this workspace is connected to. Pass the job ref as the only argument. -Usage (run from the workspace root): +Usage (run from the workspace root so the workspace context resolves): uv run .scripts/show_notebook.py jobs.onboarding_success - DLTHUB_APP_URL=https://app.dlthub.test uv run .scripts/show_notebook.py jobs.onboarding_success """ -import os import sys -import tomllib import webbrowser -from pathlib import Path - -APP_BASE = os.environ.get("DLTHUB_APP_URL", "https://app.dlthub.com").rstrip("/") if len(sys.argv) < 2: sys.exit("usage: uv run .scripts/show_notebook.py e.g. jobs.onboarding_success") ref = sys.argv[1] +from dlt._workspace._workspace_context import active +from dlt._workspace.exceptions import WorkspaceRunContextNotAvailable +from dlt_runtime.urls import workspace_url -def _find_config() -> Path: - """Locate .dlt/config.toml by walking up from this script (location-independent).""" - for parent in Path(__file__).resolve().parents: - candidate = parent / ".dlt" / "config.toml" - if candidate.is_file(): - return candidate - sys.exit("Could not find .dlt/config.toml above this script — run inside a workspace.") - - -cfg = tomllib.loads(_find_config().read_text()) -ws = cfg.get("runtime", {}).get("workspace_id") +try: + ws = active().runtime_config.workspace_id +except WorkspaceRunContextNotAvailable: + sys.exit("No workspace found here — run from the workspace root.") if not ws: sys.exit("No workspace_id in .dlt/config.toml — connect the workspace first.") -url = f"{APP_BASE}/w/{ws}/notebooks/{ref}/show?hide_header=true" +url = f"{workspace_url(ws)}/notebooks/{ref}/show?hide_header=true" print(f"Opening {url}") webbrowser.open(url) diff --git a/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/__marimo__/session/onboarding_success.py.json b/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/__marimo__/session/onboarding_success.py.json index 819f0ec..2575cbb 100644 --- a/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/__marimo__/session/onboarding_success.py.json +++ b/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/__marimo__/session/onboarding_success.py.json @@ -7,7 +7,7 @@ "cells": [ { "id": "Hbol", - "code_hash": "1a27776b2040b0805f75b129ec9774e4", + "code_hash": "a192c5210b0ffba29688aaee75b76f44", "outputs": [ { "type": "data", @@ -20,12 +20,12 @@ }, { "id": "MJUe", - "code_hash": "7ad9a3d419427dce2e8bbd1d217ebd9e", + "code_hash": "6550d5e9c367d44dff418140b3b74070", "outputs": [ { "type": "data", "data": { - "text/html": "" + "text/html": "" } } ], @@ -33,7 +33,7 @@ }, { "id": "vblA", - "code_hash": "5860d2445a93961997024114aca58422", + "code_hash": "be8b2ffd86a807ae3da2275abca5ec1e", "outputs": [ { "type": "data", @@ -85,12 +85,12 @@ }, { "id": "Xref", - "code_hash": "28e7aaedab4c496499d18c7019a7f6b1", + "code_hash": "2723352c93024b3e2aabb8dfe3046d7c", "outputs": [ { "type": "data", "data": { - "text/html": "

Congrats! Your first pipeline successfully loaded data from the sample shop API into playground.

Take a quick look at your data, then run your first query.

" + "text/html": "

Congrats! Your first pipeline successfully loaded data from the sample shop API into playground.

Take a quick look at your data, then run your first query.

" } } ], diff --git a/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/onboarding_success.py b/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/onboarding_success.py index d1c2149..72df02b 100644 --- a/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/onboarding_success.py +++ b/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/onboarding_success.py @@ -22,11 +22,10 @@ def _(): import dlt_access as da from widgets.action_button import ActionButton - from widgets.clipboard_copy import ClipboardCopy from widgets.schema_overview import SchemaOverview from widgets.scroll_cue import ScrollCue - return ActionButton, ClipboardCopy, Path, SchemaOverview, ScrollCue, asyncio, da, html, mo, np, pd + return ActionButton, Path, SchemaOverview, ScrollCue, asyncio, da, html, mo, np, pd @app.cell @@ -42,19 +41,11 @@ def _(Path, mo): "box-sizing": "border-box", } COLUMN_STYLE = { - "max-width": "800px", + "max-width": "1120px", "margin": "0 auto", "padding": "44px 24px 72px", "box-sizing": "border-box", } - WIDE_COLUMN_STYLE = {**COLUMN_STYLE, "max-width": "1120px"} - - def page_dots(current): - dots = "".join( - f'' - for i in (1, 2) - ) - return mo.Html(f'
{dots}
') def page_header(title, subtitle): return mo.Html( @@ -65,7 +56,7 @@ def page_header(title, subtitle): ) shell_css - return CARD_STYLE, COLUMN_STYLE, WIDE_COLUMN_STYLE, page_dots, page_header + return CARD_STYLE, COLUMN_STYLE, page_header @app.cell @@ -74,7 +65,7 @@ def _(ActionButton, mo): ActionButton(label="Run Query", variant="start", icon="run", size="lg") ) to_next = mo.ui.anywidget( - ActionButton(label="Next step", variant="primary", size="lg") + ActionButton(label="Next step", variant="primary", size="lg", route="org-setup") ) return run_button, to_next @@ -113,23 +104,18 @@ def _(da, get_data, mo, schema_overview): def _( CARD_STYLE, COLUMN_STYLE, - ClipboardCopy, ScrollCue, - WIDE_COLUMN_STYLE, da, html, mo, np, pd, - page_dots, page_header, run_button, schema_overview, sql_editor, to_next, ): - page = 1 + (to_next.clicks > 0) - def _spark(series): s = series.dropna() if not len(s): @@ -179,7 +165,7 @@ def _results_html(sql): "Take a quick look at your data, then run your first query.", ) - if page == 1 and (schema_overview is None or sql_editor is None): + if schema_overview is None or sql_editor is None: _rows = "".join( f'
' f'
' @@ -195,7 +181,7 @@ def _results_html(sql): "" ), ] - elif page == 1: + else: queried = run_button.clicks > 0 _schema_pane = mo.vstack([schema_overview], gap=0).style( {**CARD_STYLE, "flex": "1 1 300px", "min-width": "260px"} @@ -245,37 +231,8 @@ def _results_html(sql): ).style({"animation": "fade-in 0.3s ease"}) ) _content.append(mo.ui.anywidget(ScrollCue())) - else: - _content = [ - page_header( - "You're all set.", - "Now build your own pipeline and explore your data on dltHub.", - ), - mo.Html( - '
Your trial includes $30 of free usage.
' - '
Prompt for Claude / Codex / Cursor
' - '
' - '
Help me get started building and running a data pipeline on dltHub.
' - '" - "
", - ), - mo.ui.anywidget(ClipboardCopy()), - ] - _column = WIDE_COLUMN_STYLE if page == 1 else COLUMN_STYLE - mo.vstack([page_dots(page), *_content], gap=1.25).style(_column) + mo.vstack(_content, gap=1.25).style(COLUMN_STYLE) return @app.cell diff --git a/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/styles/head.html b/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/styles/head.html index ff1d75b..4fe2bb7 100644 --- a/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/styles/head.html +++ b/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/styles/head.html @@ -2,68 +2,3 @@ - diff --git a/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/styles/onboarding_shell.css b/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/styles/onboarding_shell.css index df3b0b1..a0da8ea 100644 --- a/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/styles/onboarding_shell.css +++ b/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/styles/onboarding_shell.css @@ -4,8 +4,6 @@ client's design tokens (runtime/clients/web globals.css). */ :root { /* dltHub brand */ - --dlt-aqua: oklch(0.7 0.1 215); /* #59c1d5 */ - --dlt-lime: oklch(0.81 0.18 115); /* #c6d300 */ --dlt-green: oklch(0.65 0.15 135); /* action / "run" */ --dlt-dark-purple: oklch(0.16 0.06 285); /* #191937 */ --dlt-medium-purple: oklch(0.28 0.09 285); @@ -36,16 +34,6 @@ #App { padding-left: 0; padding-top: 0; background: var(--dlt-bg); box-sizing: border-box; } #App .marimo-cell { background: transparent !important; border-radius: 0 !important; box-shadow: none !important; margin: 0 !important; } -/* ---- Page dots (3-step wizard progress) ---- */ -.page-dots { display: flex; justify-content: center; gap: 6px; margin: 0 0 12px; } -.page-dot { - height: 6px; border-radius: 999px; - background: color-mix(in oklch, var(--dlt-text) 12%, transparent); - transition: width 0.2s ease, background 0.2s ease; -} -.page-dot.idle { width: 6px; } -.page-dot.active { width: 18px; background: var(--dlt-accent); } - /* ---- Page header (centered title + subtitle) ---- */ .page-head { text-align: center; margin: 0; } .page-title { @@ -59,12 +47,6 @@ font-size: 18px; font-weight: 400; line-height: 1.55; color: var(--dlt-muted); margin: 14px auto 0; max-width: 560px; } -.page-trial { - font-family: var(--dlt-font); - font-size: 16px; font-weight: 400; color: var(--dlt-muted); - text-align: center; margin: 0; -} - /* ---- Results (rendered inside the query card once a query runs) ---- */ .dlt-section-label { font-family: var(--dlt-font); @@ -99,7 +81,7 @@ .run-arrow { animation: none; } } -/* ---- loading skeleton (page 2, while the playground read resolves) ---- */ +/* ---- loading skeleton (while the playground read resolves) ---- */ .dlt-skeleton { display: flex; gap: 16px; align-items: stretch; flex-wrap: wrap; animation: fade-in .25s ease; @@ -161,60 +143,6 @@ .dlt-scroll-cue.show { animation: none; } } -/* ---- copy-for-agent prompt box (page 3) ---- */ -.pb-eyebrow { - font-family: var(--dlt-font); - font-size: 16px; font-weight: 600; color: var(--dlt-text); - text-align: left; margin: 4px 0 10px; -} -.pbo-a { - --agb-fill: var(--dlt-card); - display: flex; align-items: center; justify-content: space-between; gap: 16px; - border-radius: var(--dlt-radius); padding: 14px 16px; -} -.pbo-a .pb-text { - flex: 1; min-width: 0; font-size: 16px; line-height: 1.55; - color: var(--dlt-text); text-align: left; - font-family: var(--dlt-font); -} -.dlt-prompt-copy { - flex: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; - background: var(--dlt-accent); border: 1px solid var(--dlt-accent); border-radius: 6px; - padding: 12px 24px; cursor: pointer; color: var(--dlt-accent-fg); - font: 600 16px/1 var(--dlt-font); transition: background .12s ease, border-color .12s ease; -} -.dlt-prompt-copy:hover { background: color-mix(in oklch, var(--dlt-accent) 86%, black); border-color: color-mix(in oklch, var(--dlt-accent) 86%, black); } -.dlt-prompt-copy svg { width: 18px; height: 18px; } -.dlt-prompt-copy svg path[stroke="none"] { fill: var(--dlt-accent-fg); } -.dlt-prompt-copy.copied { background: color-mix(in oklch, var(--dlt-accent) 86%, black); border-color: color-mix(in oklch, var(--dlt-accent) 86%, black); } - -/* Animated "AI" gradient border — brand conic gradient (purple→aqua→lime) drawn - in the border-box, with the fill in the padding-box. Override --agb-fill to - tint the inner surface. Mirrors the web client's signature onboarding effect. */ -@property --gradient-border-angle { - syntax: ""; - inherits: false; - initial-value: 0deg; -} -@keyframes gradient-border-rotate { - to { --gradient-border-angle: 360deg; } -} -.animated-gradient-border { - --agb-fill: var(--dlt-card); - border: 2px solid transparent; - background: - linear-gradient(var(--agb-fill), var(--agb-fill)) padding-box, - conic-gradient( - from var(--gradient-border-angle), - var(--dlt-light-purple), var(--dlt-aqua), var(--dlt-lime), - var(--dlt-aqua), var(--dlt-light-purple) - ) border-box; - animation: gradient-border-rotate 3s linear infinite; -} -@media (prefers-reduced-motion: reduce) { - .animated-gradient-border { animation: none; } -} - /* ---- Results table ---- */ .dlt-table-wrap { overflow: auto; height: 264px; box-sizing: border-box; padding: 10px 12px; } .dlt-table { diff --git a/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/widgets/action_button.py b/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/widgets/action_button.py index 7447c14..79390c3 100644 --- a/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/widgets/action_button.py +++ b/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/widgets/action_button.py @@ -3,6 +3,11 @@ Replaces marimo's default buttons (which can't be themed - shadow DOM) so the query controls match the rest of the design. Each click increments `clicks`, which marimo treats as a reactive value. + +With `route` set, a click instead posts `{type: "dlthub:navigate", route}` to +the embedding dltHub app, which validates the route against its allowlist and +navigates for us — the iframe sandbox blocks the notebook from navigating +anywhere itself. No-op when nothing is listening (standalone marimo). """ from __future__ import annotations @@ -29,6 +34,16 @@ const ic = ICONS[model.get("icon")] || ""; btn.innerHTML = ic + "" + (model.get("label") || "") + ""; btn.addEventListener("click", () => { + const route = model.get("route"); + if (route) { + // "*" is safe: the message carries no data, only a route name the + // parent checks against its own allowlist. No clicks sync — nothing + // reads it, and the trait change would re-run dependent cells. + if (window.parent !== window) { + window.parent.postMessage({ type: "dlthub:navigate", route: route }, "*"); + } + return; + } model.set("clicks", (model.get("clicks") || 0) + 1); model.save_changes(); }); @@ -76,4 +91,5 @@ class ActionButton(anywidget.AnyWidget): variant = traitlets.Unicode("secondary").tag(sync=True) icon = traitlets.Unicode("").tag(sync=True) size = traitlets.Unicode("md").tag(sync=True) + route = traitlets.Unicode("").tag(sync=True) clicks = traitlets.Int(0).tag(sync=True) diff --git a/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/widgets/clipboard_copy.py b/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/widgets/clipboard_copy.py deleted file mode 100644 index 6aee5da..0000000 --- a/src/create_dlthub_workspace/scaffolds/minimal_workspace/notebooks/onboarding_success/widgets/clipboard_copy.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Invisible anywidget that wires up copy-to-clipboard for plain HTML buttons. - -marimo's HTML renderer strips inline ``onclick`` handlers (it rebuilds the markup -as React elements), so a ``