Skip to content

Commit f236b88

Browse files
authored
Merge pull request #10 from zenml-io/feature/experimental-buttons
Add button to view dashboard after successful pipeline run / completion
2 parents 21e9ead + 90d2197 commit f236b88

File tree

6 files changed

+139
-32
lines changed

6 files changed

+139
-32
lines changed

.claude/settings.local.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(rg:*)",
5+
"Bash(grep:*)",
6+
"Bash(npm run compile:*)"
7+
],
8+
"deny": []
9+
}
10+
}

assets/main.css

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,13 +448,68 @@ footer .run-pipeline-button.running {
448448
footer .run-pipeline-button.completed {
449449
background: var(--zenml-green);
450450
color: white;
451+
cursor: default;
452+
}
453+
454+
footer .run-pipeline-button.completed:hover {
455+
background: var(--zenml-green);
456+
transform: none;
457+
box-shadow: none;
451458
}
452459

453460
footer .run-pipeline-button.failed {
454461
background: var(--zenml-red);
455462
color: white;
456463
}
457464

465+
466+
/* Pipeline button group */
467+
.pipeline-button-group {
468+
display: flex;
469+
gap: 8px;
470+
align-items: center;
471+
}
472+
473+
/* Dashboard button styling - smaller variant */
474+
.dashboard-button-small {
475+
background: var(--vscode-button-secondaryBackground);
476+
border: 1px solid var(--vscode-sideBar-border);
477+
color: var(--vscode-button-secondaryForeground);
478+
padding: 9px 16px;
479+
border-radius: 6px;
480+
font-size: 14px;
481+
font-weight: 500;
482+
cursor: pointer;
483+
transition: all 0.3s ease;
484+
display: flex;
485+
align-items: center;
486+
gap: 6px;
487+
justify-content: center;
488+
font-family: "Inter", sans-serif;
489+
margin: 0;
490+
height: 38px;
491+
text-decoration: none;
492+
flex-shrink: 0;
493+
white-space: nowrap;
494+
box-sizing: border-box;
495+
}
496+
497+
.dashboard-button-small:hover {
498+
background: var(--vscode-button-secondaryHoverBackground);
499+
transform: translateY(-1px);
500+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
501+
color: var(--vscode-button-secondaryForeground);
502+
text-decoration: none;
503+
}
504+
505+
.dashboard-button-small:active {
506+
transform: translateY(0);
507+
}
508+
509+
.dashboard-button-small .codicon {
510+
font-size: 14px;
511+
}
512+
458513
#progress {
459514
height: 4px;
460515
background-color: var(--vscode-progressBar-background);

assets/main.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,8 @@
476476
} else if (status === "completed" || status === "cached") {
477477
runButton.innerHTML = `<i class="checkmark">✓</i> ${buttonTexts[status]}`;
478478
//@ts-ignore
479-
runButton.disabled = false;
479+
runButton.disabled = true;
480+
runButton.style.cursor = "default";
480481
} else if (status === "failed") {
481482
runButton.innerHTML = `<i class="codicon codicon-error"></i> ${buttonTexts[status]}`;
482483
//@ts-ignore
@@ -493,14 +494,18 @@
493494
}
494495

495496
function showDashboardUrl(url) {
496-
const dashboardLink = document.getElementById("dashboard-link");
497-
const dashboardUrl = document.getElementById("dashboard-url");
497+
const dashboardButton = document.getElementById("dashboard-button");
498498

499-
if (dashboardLink && dashboardUrl) {
499+
if (dashboardButton) {
500500
//@ts-ignore
501-
dashboardUrl.href = url;
502-
dashboardUrl.textContent = "View Pipeline in Dashboard";
503-
dashboardLink.style.display = "flex";
501+
dashboardButton.href = url;
502+
dashboardButton.style.display = "flex";
503+
504+
// Add click handler to open in external browser
505+
dashboardButton.addEventListener('click', function(e) {
506+
e.preventDefault();
507+
vscode.postMessage({ type: "openDashboard", url: url });
508+
});
504509
}
505510
}
506511

src/tutorialOrchestrator.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,14 @@ export default class TutorialOrchestrator {
2727
private _getDashboardUrl(runId?: string): string {
2828
const baseUrl = vscode.workspace
2929
.getConfiguration("zenml")
30-
.get<string>("dashboardUrl", "http://localhost:8080");
31-
return runId ? `${baseUrl}/workspaces/default/runs/${runId}` : baseUrl;
30+
.get<string>("dashboardUrl", "https://cloud.zenml.io");
31+
// If we have a run ID, show specific run, otherwise show pipelines page
32+
if (runId) {
33+
return `${baseUrl}/workspaces/default/runs/${runId}`;
34+
} else {
35+
// Fallback to generic pipelines page when no specific run ID
36+
return `${baseUrl}/workspaces/default/pipelines`;
37+
}
3238
}
3339

3440
constructor(context: vscode.ExtensionContext, tutorial: Tutorial) {
@@ -142,7 +148,7 @@ export default class TutorialOrchestrator {
142148
codeRunner(
143149
this.terminal,
144150
this._codePanel.document.uri,
145-
() => {
151+
(dashboardUrl?: string) => {
146152
vscode.window.showInformationMessage("Code Ran Successfully! 🎉");
147153
if (callback) {
148154
callback();
@@ -196,27 +202,25 @@ export default class TutorialOrchestrator {
196202
codeRunner(
197203
this.terminal,
198204
this._codePanel.document.uri,
199-
(runId?: string) => {
205+
(dashboardUrl?: string) => {
200206
// Pipeline completed successfully
201207
this._pipelineRunning = false;
202208
this._completedTutorials.add(this._tutorial.currentSection.index);
203209
this._sendWebviewMessage({
204210
type: "pipelineStatusUpdate",
205211
status: "completed",
206212
});
207-
this._sendWebviewMessage({ type: "pipelineCompleted", runId: runId });
213+
this._sendWebviewMessage({ type: "pipelineCompleted" });
208214

209215
// Save progress
210216
this._saveProgress();
211217

212-
// Show dashboard URL
213-
if (runId) {
214-
const dashboardUrl = this._getDashboardUrl(runId);
215-
this._sendWebviewMessage({
216-
type: "showDashboardUrl",
217-
url: dashboardUrl,
218-
});
219-
}
218+
// Show dashboard URL - use the captured URL or fallback to generic
219+
const finalDashboardUrl = dashboardUrl || this._getDashboardUrl();
220+
this._sendWebviewMessage({
221+
type: "showDashboardUrl",
222+
url: finalDashboardUrl,
223+
});
220224
},
221225
() => {
222226
// Pipeline failed
@@ -779,10 +783,16 @@ export default class TutorialOrchestrator {
779783
<i class="codicon codicon-chevron-left"></i>
780784
<span>Prev</span>
781785
</button>
782-
<button class="run-pipeline-button" id="run-pipeline">
783-
<i class="codicon codicon-play"></i>
784-
<span>Run Pipeline</span>
785-
</button>
786+
<div class="pipeline-button-group">
787+
<button class="run-pipeline-button" id="run-pipeline">
788+
<i class="codicon codicon-play"></i>
789+
<span>Run Pipeline</span>
790+
</button>
791+
<a class="dashboard-button-small" id="dashboard-button" href="#" style="display: none;">
792+
<i class="codicon codicon-link-external"></i>
793+
<span>View in dashboard</span>
794+
</a>
795+
</div>
786796
<button class="footer-nav-button next ${
787797
isLast && !isCompletionScreen ? "disabled" : ""
788798
}" id="nav-next" ${isLast && !isCompletionScreen ? "disabled" : ""}>

src/utils/codeRunner.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { unlinkSync, writeFileSync } from "fs";
1+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
22
import os from "os";
33
import path from "path";
44
import * as vscode from "vscode";
@@ -7,8 +7,8 @@ import getNonce from "./getNonce";
77
export default function codeRunner(
88
terminal: vscode.Terminal,
99
fileUri: vscode.Uri,
10-
onSuccessCallback?: Function,
11-
onErrorCallback?: Function
10+
onSuccessCallback?: (dashboardUrl?: string) => void,
11+
onErrorCallback?: () => void
1212
) {
1313
const uniqueId = getNonce();
1414

@@ -34,6 +34,9 @@ function runCode(
3434
// Get the project root directory (go up from pipeline file to project root)
3535
const projectRoot = path.resolve(path.dirname(filePath), "../..");
3636

37+
const dashboardUrlFile = path.join(os.tmpdir(), `dashboard_url_${uniqueId}.txt`);
38+
const pipelineOutputLog = path.join(os.tmpdir(), `pipeline_output_${uniqueId}.log`);
39+
3740
writeFileSync(
3841
scriptPath,
3942
`
@@ -42,8 +45,17 @@ function runCode(
4245
echo "Executing code..."
4346
cd "${projectRoot}"
4447
export PYTHONPATH="${projectRoot}:$PYTHONPATH"
45-
python "${filePath}"
46-
if [ $? -eq 0 ]; then
48+
# Capture output to extract dashboard URL
49+
python "${filePath}" 2>&1 | tee "${pipelineOutputLog}"
50+
RESULT=$?
51+
52+
# Extract dashboard URL if present
53+
grep "DASHBOARD_URL:" "${pipelineOutputLog}" | tail -1 | cut -d':' -f2- > "${dashboardUrlFile}"
54+
55+
# Clean up
56+
rm -f "${pipelineOutputLog}"
57+
58+
if [ $RESULT -eq 0 ]; then
4759
touch "${successFilePath}"
4860
else
4961
touch "${errorFilePath}"
@@ -59,8 +71,8 @@ function runCode(
5971
function initializeFileWatcher(
6072
filePath: string,
6173
uniqueId: string,
62-
onSuccessCallback?: Function,
63-
onFailureCallback?: Function
74+
onSuccessCallback?: (dashboardUrl?: string) => void,
75+
onFailureCallback?: () => void
6476
) {
6577
const removeLastFileFromPath = (filePath: string) => {
6678
let sections = filePath.split("/");
@@ -87,8 +99,21 @@ function initializeFileWatcher(
8799
vscode.workspace.fs.delete(uri);
88100
unlinkSync(scriptPath);
89101
watcher.dispose();
102+
103+
// Try to read dashboard URL if available
104+
const dashboardUrlFile = path.join(os.tmpdir(), `dashboard_url_${uniqueId}.txt`);
105+
let dashboardUrl: string | undefined;
106+
try {
107+
if (existsSync(dashboardUrlFile)) {
108+
dashboardUrl = readFileSync(dashboardUrlFile, 'utf8').trim();
109+
unlinkSync(dashboardUrlFile);
110+
}
111+
} catch (error) {
112+
// Ignore errors reading dashboard URL
113+
}
114+
90115
if (onSuccessCallback) {
91-
onSuccessCallback();
116+
onSuccessCallback(dashboardUrl);
92117
}
93118
} else if (uri.fsPath.endsWith(errorFileName)) {
94119
vscode.workspace.fs.delete(uri);

utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ def log_dashboard_urls(pipeline_name: str):
2525
logger.info(f"🌐 View this run: {dashboard_url}")
2626
logger.info(f"🌐 View all artifacts: {base_url}/artifacts")
2727
logger.info("=" * 60)
28+
# Print dashboard URL for VSCode extension to capture
29+
print(f"DASHBOARD_URL:{dashboard_url}")

0 commit comments

Comments
 (0)