From 02fa622dcd47128c7f1b6cee7d8bfb665de49004 Mon Sep 17 00:00:00 2001 From: Camiel van Schoonhoven Date: Thu, 11 Jun 2026 12:31:10 -0700 Subject: [PATCH 1/2] feat: Guided Tour - Secrets --- .../Secrets.pipeline.component.yaml | 155 ++++++++++++++++++ .../Learn/tours/usingSecrets.tour.json | 139 ++++++++++++++++ .../components/SubmitTaskArgumentsDialog.tsx | 2 + .../components/ThunderMenu/ThunderMenu.tsx | 1 + .../components/DynamicDataSubmenu.tsx | 5 +- 5 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 public/tour-pipelines/Secrets.pipeline.component.yaml create mode 100644 src/components/Learn/tours/usingSecrets.tour.json diff --git a/public/tour-pipelines/Secrets.pipeline.component.yaml b/public/tour-pipelines/Secrets.pipeline.component.yaml new file mode 100644 index 000000000..c97df4315 --- /dev/null +++ b/public/tour-pipelines/Secrets.pipeline.component.yaml @@ -0,0 +1,155 @@ +name: Secrets +description: | + A tiny authenticated pipeline used by the "Using Secrets" guided tour. The + Authenticated Request task reads a bearer token and an API key, sends them as + headers to https://httpbin.org/headers, and prints a masked response. Neither + credential is ever written into the pipeline definition: assign a secret to the + task's `token` argument with the lightning-bolt menu, and bind a secret to the + `API_KEY` input when you submit. +metadata: + annotations: + editor.flow-direction: left-to-right +inputs: + - name: API_KEY + description: | + API key for the request. Left empty here so you can bind a secret to it at + submit time via run arguments. + optional: true + annotations: + editor.position: '{"x": -180, "y": 120}' +outputs: + - name: HTTP Response + annotations: + editor.position: '{"x": 920, "y": 80}' +implementation: + graph: + tasks: + Authenticated Request: + componentRef: + name: Authenticated Request + spec: + name: Authenticated Request + description: | + Reads a bearer token and an API key (each injected from a Tangle + secret as a file), sends them as request headers to + https://httpbin.org/headers, masks the sensitive headers, and writes + the response body to its output. Credential values are never logged. + inputs: + - name: token + description: Bearer token, injected from a Tangle secret. + optional: true + - name: api_key + description: API key, injected from a Tangle secret. + optional: true + outputs: + - name: response + type: String + description: httpbin's JSON response body, with secrets masked. + implementation: + container: + image: python:3.12-slim + command: + - python3 + - '-u' + - '-c' + - | + import argparse, json, os, urllib.request + + parser = argparse.ArgumentParser() + parser.add_argument("--token") + parser.add_argument("--api-key", dest="api_key") + parser.add_argument("--output", required=True) + args = parser.parse_args() + + headers = {} + if args.token: + with open(args.token) as f: + token = f.read().strip() + headers["Authorization"] = f"Bearer {token}" + print(f"loaded token: length={len(token)} (value not logged)") + if args.api_key: + with open(args.api_key) as f: + api_key = f.read().strip() + headers["X-Api-Key"] = api_key + print(f"loaded api_key: length={len(api_key)} (value not logged)") + + req = urllib.request.Request( + "https://httpbin.org/headers", headers=headers + ) + with urllib.request.urlopen(req, timeout=30) as resp: + print(f"http status: {resp.status}") + body = resp.read().decode("utf-8") + + parsed = json.loads(body) + # httpbin echoes the request headers back, so mask the sensitive + # ones before writing them to the pipeline output. + echoed = parsed.get("headers", {}) + if "Authorization" in echoed: + echoed["Authorization"] = "Bearer " + if "X-Api-Key" in echoed: + echoed["X-Api-Key"] = "" + masked = json.dumps(parsed, indent=2) + print(masked) + + os.makedirs(os.path.dirname(args.output), exist_ok=True) + with open(args.output, "w") as f: + f.write(masked + "\n") + args: + - if: + cond: + isPresent: token + then: + - '--token' + - inputPath: token + - if: + cond: + isPresent: api_key + then: + - '--api-key' + - inputPath: api_key + - '--output' + - outputPath: response + arguments: + api_key: + graphInput: + inputName: API_KEY + annotations: + editor.position: '{"x": 160, "y": 80}' + cloud-pipelines.net/launchers/generic/resources.cpu: '1' + cloud-pipelines.net/launchers/generic/resources.memory: 512Mi + Show Response: + componentRef: + name: Show Response + spec: + name: Show Response + description: Echoes the masked HTTP response to stdout and to the pipeline output. + inputs: + - name: response + type: String + outputs: + - name: echoed + type: String + implementation: + container: + image: python:3.12-slim + command: + - sh + - '-ec' + - | + cat "$0" + mkdir -p "$(dirname "$1")" + cp "$0" "$1" + - inputPath: response + - outputPath: echoed + arguments: + response: + taskOutput: + outputName: response + taskId: Authenticated Request + annotations: + editor.position: '{"x": 560, "y": 80}' + outputValues: + HTTP Response: + taskOutput: + outputName: echoed + taskId: Show Response diff --git a/src/components/Learn/tours/usingSecrets.tour.json b/src/components/Learn/tours/usingSecrets.tour.json new file mode 100644 index 000000000..d47bdd0f1 --- /dev/null +++ b/src/components/Learn/tours/usingSecrets.tour.json @@ -0,0 +1,139 @@ +{ + "id": "using-secrets", + "displayName": "Guided Tour: Using Secrets", + "requiresEditor": true, + "starterPipelineUrl": "tour-pipelines/Secrets.pipeline.component.yaml", + "steps": [ + { + "selector": "[data-tour-anchor=\"no-spotlight\"]", + "content": "Let's talk about **Secrets**.\n\nEverything you put in a pipeline (argument values, input defaults, run settings) is saved with it and visible to anyone who can access your Tangle instance. If they can open this pipeline or one of its runs, they can read those values.\n\nSo typing an API key or token straight into an argument quietly shares it with everyone, and writes it into the pipeline YAML too.", + "position": "center" + }, + { + "selector": "[data-tour-anchor=\"no-spotlight\"]", + "content": "That's where **secrets** come in.\n\nA secret keeps its value on the backend under a name you choose. Tangle only ever stores and sends the **name**. The value gets injected into the running container when the pipeline runs, and it never shows up anywhere in the app.", + "position": "center" + }, + { + "selector": "[data-tour-card=\"task\"][data-tour-card-name=\"Authenticated Request\"]", + "highlightedSelectors": [ + "[data-tour-card=\"task\"][data-tour-card-name=\"Authenticated Request\"]" + ], + "resizeObservables": ["[data-tour=\"editor-canvas\"]"], + "content": "Let's create a secret and put it to use right in the **argument editor**.\n\nStart by selecting the **Authenticated Request** task. Its inputs show up in the **Task Properties** panel on the right.", + "position": "top", + "stepInteraction": true, + "interaction": "select-task", + "targetTaskName": "Authenticated Request" + }, + { + "selector": "[data-dock-window-content=\"context-panel\"]", + "highlightedSelectors": [ + "[data-dock-window-content=\"context-panel\"]", + "[data-tour=\"thunder-menu-content\"]", + "[data-tour=\"thunder-menu-submenu-content\"]" + ], + "mutationObservables": [ + "[data-dock-window-content=\"context-panel\"]", + "[data-tour=\"thunder-menu-content\"]", + "[data-tour=\"thunder-menu-submenu-content\"]" + ], + "resizeObservables": ["[data-dock-window-content=\"context-panel\"]"], + "requiresTaskSelected": "Authenticated Request", + "content": "Now open the secret picker for the `token` argument.\n\nHover its row to reveal the **⚡** button, click it, then choose **Dynamic Data → Select Secret**.", + "position": "left", + "stepInteraction": true, + "interaction": "open-secret-dialog" + }, + { + "selector": "[data-testid=\"select-secret-dialog\"]", + "highlightedSelectors": ["[data-testid=\"select-secret-dialog\"]"], + "mutationObservables": ["[data-testid=\"select-secret-dialog\"]"], + "resizeObservables": ["[data-testid=\"select-secret-dialog\"]"], + "content": "Click **Add Secret** and give it a name and a value. The value is write only, so Tangle won't ever show it back to you. Save it, then pick it from the list to attach it to the argument.\n\nOnly the secret's **name** ends up in the pipeline.", + "position": "left", + "stepInteraction": true, + "interaction": "assign-secret-argument", + "targetArgumentName": "token" + }, + { + "selector": "[data-dock-window-content=\"context-panel\"]", + "highlightedSelectors": ["[data-dock-window-content=\"context-panel\"]"], + "mutationObservables": ["[data-dock-window-content=\"context-panel\"]"], + "resizeObservables": ["[data-dock-window-content=\"context-panel\"]"], + "content": "Nice. The `token` argument now shows your secret **by name**, with a little lock icon, instead of its value.\n\nThe value stays on the backend and the pipeline only points at the name. Anyone you share this with sees the reference, never the credential.", + "position": "left" + }, + { + "selector": "[data-tracking-id=\"v2.header.settings\"]", + "highlightedSelectors": ["[data-tour=\"editor-top-bar-actions\"]"], + "ringSelectors": ["[data-tracking-id=\"v2.header.settings\"]"], + "tourPanel": "secrets-manager", + "content": "Tangle has a dedicated **secrets manager**.\n\nOpen it by clicking the **Settings** gear in the top right.", + "position": "bottom", + "stepInteraction": true, + "interaction": "open-settings-panel" + }, + { + "selector": "[data-tour=\"tour-settings-dialog\"]", + "highlightedSelectors": ["[data-tour=\"tour-settings-dialog\"]"], + "mutationObservables": ["[data-tour=\"tour-settings-dialog\"]"], + "resizeObservables": ["[data-tour=\"tour-settings-dialog\"]"], + "tourPanel": "secrets-manager", + "content": "This is **Settings → Secrets**, where all your secrets live. They're listed by name only, never by value.\n\nYou can **replace** a secret's value here (the change reaches every pipeline that uses it, and the value still never appears) or **delete** one you no longer need. Take a look, then carry on." + }, + { + "selector": "[data-tour-card=\"input\"][data-tour-card-name=\"API_KEY\"]", + "highlightedSelectors": [ + "[data-tour-card=\"input\"][data-tour-card-name=\"API_KEY\"]" + ], + "resizeObservables": ["[data-tour=\"editor-canvas\"]"], + "content": "There's a second way to hand a secret to a pipeline, and it keeps the pipeline itself generic.\n\nInstead of baking a specific secret into a task, you can leave a pipeline **input** empty and choose a secret for it **when you submit a run**. The pipeline definition then mentions no secret at all, and everyone can run it with their own.\n\nThis pipeline's **API_KEY** input was left blank for exactly that.", + "position": "top" + }, + { + "selector": "[data-dock-window-content=\"runs-and-submission\"]", + "highlightedSelectors": [ + "[data-dock-window-content=\"runs-and-submission\"]" + ], + "ringSelectors": ["[data-testid=\"run-with-arguments-button\"]"], + "mutationObservables": [ + "[data-dock-window-content=\"runs-and-submission\"]" + ], + "resizeObservables": [ + "[data-dock-window-content=\"runs-and-submission\"]" + ], + "ensureWindowRestored": "runs-and-submission", + "content": "In **Runs and submission**, click the **split-arrows** icon next to Submit to open the run arguments dialog.", + "position": "right", + "stepInteraction": true, + "interaction": "open-submit-dialog" + }, + { + "selector": "[data-tour=\"submit-arguments-dialog\"]", + "highlightedSelectors": [ + "[data-tour=\"submit-arguments-dialog\"]", + "[data-testid=\"select-secret-dialog\"]" + ], + "mutationObservables": [ + "[data-tour=\"submit-arguments-dialog\"]", + "[data-testid=\"select-secret-dialog\"]" + ], + "resizeObservables": ["[data-tour=\"submit-arguments-dialog\"]"], + "content": "Find the **API_KEY** input, hover it, and click the **lock** (\"Use Secret\") button. Pick your secret to set it for this run. The value is supplied only at submit time and never saved into the pipeline.", + "position": "left", + "stepInteraction": true, + "interaction": "assign-secret-submit", + "targetArgumentName": "API_KEY" + }, + { + "selector": "[data-tour=\"submit-arguments-dialog\"]", + "highlightedSelectors": ["[data-tour=\"submit-arguments-dialog\"]"], + "ringSelectors": ["[data-tour=\"submit-arguments-confirm\"]"], + "mutationObservables": ["[data-tour=\"submit-arguments-dialog\"]"], + "resizeObservables": ["[data-tour=\"submit-arguments-dialog\"]"], + "content": "Great! Now the pipeline will use the selected secret for the **API_KEY** input at runtime and the value will never be recorded in the pipeline definition.\n\nClick **Submit Run** to launch the pipeline with your secret, then wrap up the tour.", + "position": "left" + } + ] +} diff --git a/src/components/shared/Submitters/Tangle/components/SubmitTaskArgumentsDialog.tsx b/src/components/shared/Submitters/Tangle/components/SubmitTaskArgumentsDialog.tsx index 9a3f183f1..fb86a3dc2 100644 --- a/src/components/shared/Submitters/Tangle/components/SubmitTaskArgumentsDialog.tsx +++ b/src/components/shared/Submitters/Tangle/components/SubmitTaskArgumentsDialog.tsx @@ -144,6 +144,7 @@ export const SubmitTaskArgumentsDialog = ({ event.preventDefault(), @@ -216,6 +217,7 @@ export const SubmitTaskArgumentsDialog = ({ diff --git a/src/routes/v2/pages/Editor/components/ArgumentRow/components/ThunderMenu/ThunderMenu.tsx b/src/routes/v2/pages/Editor/components/ArgumentRow/components/ThunderMenu/ThunderMenu.tsx index 883bfb407..e2a16db96 100644 --- a/src/routes/v2/pages/Editor/components/ArgumentRow/components/ThunderMenu/ThunderMenu.tsx +++ b/src/routes/v2/pages/Editor/components/ArgumentRow/components/ThunderMenu/ThunderMenu.tsx @@ -115,6 +115,7 @@ export const ThunderMenu = observer(function ThunderMenu({ side="bottom" sideOffset={4} className="w-56 z-[9999]" + data-tour="thunder-menu-content" > Dynamic Data - + {groups.map((group, index) => ( {index > 0 && } From f386c6389234fc8f9d9cb1ead965864e4a9f480d Mon Sep 17 00:00:00 2001 From: Camiel van Schoonhoven Date: Fri, 12 Jun 2026 14:59:56 -0700 Subject: [PATCH 2/2] feat: Guided Tour (Secrets) - opt into in-memory mock backend Set mockBackend on the Secrets tour so it runs hands-on without a real backend (secrets mocked in-memory), and reword the final step so it reads correctly whether or not a run is actually submitted. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/Learn/tours/usingSecrets.tour.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Learn/tours/usingSecrets.tour.json b/src/components/Learn/tours/usingSecrets.tour.json index d47bdd0f1..b68d4c6b3 100644 --- a/src/components/Learn/tours/usingSecrets.tour.json +++ b/src/components/Learn/tours/usingSecrets.tour.json @@ -3,6 +3,7 @@ "displayName": "Guided Tour: Using Secrets", "requiresEditor": true, "starterPipelineUrl": "tour-pipelines/Secrets.pipeline.component.yaml", + "mockBackend": true, "steps": [ { "selector": "[data-tour-anchor=\"no-spotlight\"]", @@ -132,7 +133,7 @@ "ringSelectors": ["[data-tour=\"submit-arguments-confirm\"]"], "mutationObservables": ["[data-tour=\"submit-arguments-dialog\"]"], "resizeObservables": ["[data-tour=\"submit-arguments-dialog\"]"], - "content": "Great! Now the pipeline will use the selected secret for the **API_KEY** input at runtime and the value will never be recorded in the pipeline definition.\n\nClick **Submit Run** to launch the pipeline with your secret, then wrap up the tour.", + "content": "Done! The **API_KEY** input is now backed by your secret. At submit time the value is supplied to the run and never recorded in the pipeline definition.\n\nWith a backend connected you can **Submit Run** to launch it with your secret. Either way, wrap up the tour below.", "position": "left" } ]