Skip to content

Commit

Permalink
Merge pull request #24 from frye-t/feature/dashboard-links
Browse files Browse the repository at this point in the history
Add sidebar links to open dashboard for stacks and runs
  • Loading branch information
strickvl authored May 24, 2024
2 parents 70bc167 + 4d3d986 commit 8dacf71
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 2 deletions.
24 changes: 23 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,12 @@
"icon": "$(copy)",
"category": "ZenML"
},
{
"command": "zenml.goToStackUrl",
"title": "Go to URL",
"icon": "$(globe)",
"category": "ZenML"
},
{
"command": "zenml.setPipelineRunsPerPage",
"title": "Set Runs Per Page",
Expand All @@ -217,6 +223,12 @@
"icon": "$(trash)",
"category": "ZenML Pipeline Runs"
},
{
"command": "zenml.goToPipelineUrl",
"title": "Go to URL",
"icon": "$(globe)",
"category": "ZenML Pipeline Runs"
},
{
"command": "zenml.refreshEnvironmentView",
"title": "Refresh Environment View",
Expand Down Expand Up @@ -328,10 +340,20 @@
"command": "zenml.copyStack",
"group": "inline@3"
},
{
"when": "stackCommandsRegistered && view == zenmlStackView && viewItem == stack",
"command": "zenml.goToStackUrl",
"group": "inline@4"
},
{
"when": "pipelineCommandsRegistered && view == zenmlPipelineView && viewItem == pipelineRun",
"command": "zenml.deletePipelineRun",
"group": "inline"
"group": "inline@1"
},
{
"when": "pipelineCommandsRegistered && view == zenmlPipelineView && viewItem == pipelineRun",
"command": "zenml.goToPipelineUrl",
"group": "inline@2"
},
{
"when": "environmentCommandsRegistered && view == zenmlEnvironmentView && viewItem == interpreter",
Expand Down
2 changes: 2 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
"command.renameStack": "Renames a specified ZenML stack.",
"command.setActiveStack": "Sets a specified stack as the active ZenML stack.",
"command.copyStack": "Creates a copy of a specified ZenML stack.",
"command.goToStackUrl": "Opens the URL of the specific ZenML stack in the Dashboard",
"command.refreshPipelineView": "Refreshes the pipeline view to display the latest information on ZenML pipeline runs.",
"command.setPipelineRunsPerPage": "Sets the number of pipeline runs to display per page in the pipeline view.",
"command.nextPipelineRunsPage": "Navigates to the next page of pipeline runs in the pipeline view.",
"command.previousPipelineRunsPage": "Navigates to the previous page of pipeline runs in the pipeline view.",
"command.deletePipelineRun": "Deletes a specified ZenML pipeline run.",
"command.goToPipelineUrl": "Opens the URL of the specific ZenML pipeline in the Dashboard",
"command.setPythonInterpreter": "Sets the Python interpreter for ZenML-related tasks within the VS Code environment.",
"command.refreshEnvironmentView": "Updates the Environment View with the latest ZenML configuration and system information.",
"command.restartLspServer": "Restarts the Language Server to ensure the latest configurations are used.",
Expand Down
23 changes: 23 additions & 0 deletions src/commands/pipelines/cmds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { showErrorMessage, showInformationMessage } from '../../utils/notificati
import { PipelineTreeItem } from '../../views/activityBar';
import { PipelineDataProvider } from '../../views/activityBar/pipelineView/PipelineDataProvider';
import * as vscode from 'vscode';
import { getPipelineRunDashboardUrl } from './utils';

/**
* Triggers a refresh of the pipeline view within the UI components.
Expand Down Expand Up @@ -72,7 +73,29 @@ const deletePipelineRun = async (node: PipelineTreeItem): Promise<void> => {
}
};

/**
* Opens the selected pipieline run in the ZenML Dashboard in the browser
*
* @param {PipelineTreeItem} node The pipeline run to open.
*/
const goToPipelineUrl = (node: PipelineTreeItem): void => {
const url = getPipelineRunDashboardUrl(node.id);

if (url) {
try {
const parsedUrl = vscode.Uri.parse(url);

vscode.env.openExternal(parsedUrl);
vscode.window.showInformationMessage(`Opening: ${url}`);
} catch (error) {
console.log(error);
vscode.window.showErrorMessage(`Failed to open pipeline run URL: ${error}`);
}
}
};

export const pipelineCommands = {
refreshPipelineView,
deletePipelineRun,
goToPipelineUrl,
};
3 changes: 3 additions & 0 deletions src/commands/pipelines/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export const registerPipelineCommands = (context: ExtensionContext) => {
'zenml.deletePipelineRun',
async (node: PipelineTreeItem) => await pipelineCommands.deletePipelineRun(node)
),
registerCommand(
'zenml.goToPipelineUrl',
async (node:PipelineTreeItem) => await pipelineCommands.goToPipelineUrl(node)),
registerCommand('zenml.nextPipelineRunsPage', async () =>
pipelineDataProvider.goToNextPage()
),
Expand Down
36 changes: 36 additions & 0 deletions src/commands/pipelines/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright(c) ZenML GmbH 2024. All Rights Reserved.
// Licensed under the Apache License, Version 2.0(the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied.See the License for the specific language governing
// permissions and limitations under the License.
import { getZenMLServerUrl } from '../../utils/global';

/**
* Gets the Dashboard URL for the corresponding ZenML pipeline run
*
* @param {string} id - The id of the ZenML pipeline run to be opened
* @returns {string} - The URL corresponding to the pipeline run in the ZenML Dashboard
*/
export const getPipelineRunDashboardUrl = (id: string): string => {
const PIPELINE_URL_STUB = "SERVER_URL/workspaces/default/all-runs/PIPELINE_ID/dag";
const currentServerUrl = getZenMLServerUrl();

const pipelineUrl = PIPELINE_URL_STUB
.replace("SERVER_URL", currentServerUrl)
.replace("PIPELINE_ID", id);

return pipelineUrl;
};

const pipelineUtils = {
getPipelineRunDashboardUrl,
};

export default pipelineUtils;
24 changes: 23 additions & 1 deletion src/commands/stack/cmds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import * as vscode from 'vscode';
import { StackDataProvider, StackTreeItem } from '../../views/activityBar';
import ZenMLStatusBar from '../../views/statusBar';
import { switchActiveStack } from './utils';
import { getStackDashboardUrl, switchActiveStack } from './utils';
import { LSClient } from '../../services/LSClient';
import { showInformationMessage } from '../../utils/notifications';

Expand Down Expand Up @@ -157,10 +157,32 @@ const setActiveStack = async (node: StackTreeItem): Promise<void> => {
);
};

/**
* Opens the selected stack in the ZenML Dashboard in the browser
*
* @param {StackTreeItem} node The stack to open.
*/
const goToStackUrl = (node: StackTreeItem) => {
const url = getStackDashboardUrl(node.id);

if (url) {
try {
const parsedUrl = vscode.Uri.parse(url);

vscode.env.openExternal(parsedUrl);
vscode.window.showInformationMessage(`Opening: ${url}`);
} catch (error) {
console.log(error);
vscode.window.showErrorMessage(`Failed to open stack URL: ${error}`);
}
}
};

export const stackCommands = {
refreshStackView,
refreshActiveStack,
renameStack,
copyStack,
setActiveStack,
goToStackUrl
};
4 changes: 4 additions & 0 deletions src/commands/stack/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { stackCommands } from './cmds';
import { registerCommand } from '../../common/vscodeapi';
import { ZenExtension } from '../../services/ZenExtension';
import { ExtensionContext, commands, window } from 'vscode';
import { node } from 'webpack';

/**
* Registers stack-related commands for the extension.
Expand Down Expand Up @@ -42,6 +43,9 @@ export const registerStackCommands = (context: ExtensionContext) => {
'zenml.setActiveStack',
async (node: StackTreeItem) => await stackCommands.setActiveStack(node)
),
registerCommand(
'zenml.goToStackUrl',
async (node: StackTreeItem) => await stackCommands.goToStackUrl(node)),
registerCommand(
'zenml.copyStack',
async (node: StackTreeItem) => await stackCommands.copyStack(node)
Expand Down
19 changes: 19 additions & 0 deletions src/commands/stack/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as vscode from 'vscode';
import { LSClient } from '../../services/LSClient';
import { GetActiveStackResponse, SetActiveStackResponse } from '../../types/LSClientResponseTypes';
import { showErrorMessage } from '../../utils/notifications';
import { getZenMLServerUrl } from '../../utils/global';

/**
* Switches the active ZenML stack to the specified stack name.
Expand Down Expand Up @@ -81,10 +82,28 @@ export const getActiveStackIdFromConfig = (): string | undefined => {
return config.get<string>('activeStackId');
};

/**
* Gets the Dashboard URL for the corresponding ZenML stack
*
* @param {string} id - The id of the ZenML stack to be opened
* @returns {string} - The URL corresponding to the pipeline in the ZenML Dashboard
*/
export const getStackDashboardUrl = (id: string): string => {
const STACK_URL_STUB = "SERVER_URL/workspaces/default/stacks/STACK_ID/configuration";
const currentServerUrl = getZenMLServerUrl();

const stackUrl = STACK_URL_STUB
.replace("SERVER_URL", currentServerUrl)
.replace("STACK_ID", id);

return stackUrl;
};

const stackUtils = {
switchActiveStack,
getActiveStack,
storeActiveStack,
getStackDashboardUrl,
};

export default stackUtils;
21 changes: 21 additions & 0 deletions src/test/ts_tests/commands/stackCommands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { MockEventBus } from '../__mocks__/MockEventBus';
import { MockLSClient } from '../__mocks__/MockLSClient';
import { LSClient } from '../../../services/LSClient';
import { EventBus } from '../../../services/EventBus';
import * as globalUtils from '../../../utils/global';
import stackUtils from '../../../commands/stack/utils';
import { MockStackDataProvider, MockZenMLStatusBar } from '../__mocks__/MockViewProviders';

Expand All @@ -40,13 +41,15 @@ suite('Stack Commands Test Suite', () => {
mockLSClient = new MockLSClient(mockEventBus);
mockStackDataProvider = new MockStackDataProvider();
mockStatusBar = new MockZenMLStatusBar();
const stubbedServerUrl = 'http://mocked-server.com';

// Stub classes to return mock instances
sandbox.stub(StackDataProvider, 'getInstance').returns(mockStackDataProvider);
sandbox.stub(ZenMLStatusBar, 'getInstance').returns(mockStatusBar);
sandbox.stub(LSClient, 'getInstance').returns(mockLSClient);
sandbox.stub(EventBus, 'getInstance').returns(mockEventBus);
sandbox.stub(stackUtils, 'storeActiveStack').resolves();
sandbox.stub(globalUtils, 'getZenMLServerUrl').returns(stubbedServerUrl);

showInputBoxStub = sandbox.stub(vscode.window, 'showInputBox');
showInformationMessageStub = sandbox.stub(vscode.window, 'showInformationMessage');
Expand Down Expand Up @@ -106,6 +109,24 @@ suite('Stack Commands Test Suite', () => {
);
});

test('goToStackUrl opens the correct URL and shows an information message', () => {
const stackId = 'stack-id-123';
const expectedUrl = stackUtils.getStackDashboardUrl(stackId);

const openExternalStub = sandbox.stub(vscode.env, 'openExternal');

stackCommands.goToStackUrl({ label: 'Stack', id: stackId } as any);

assert.strictEqual(openExternalStub.calledOnce, true, 'openExternal should be called once');
assert.strictEqual(openExternalStub.args[0][0].toString(), expectedUrl, 'Correct URL should be passed to openExternal');
assert.strictEqual(showInformationMessageStub.calledOnce, true, 'showInformationMessage should be called once');
assert.strictEqual(
showInformationMessageStub.args[0][0],
`Opening: ${expectedUrl}`,
'Correct information message should be shown'
);
});

test('stackDataProviderMock.refresh can be called directly', async () => {
await mockStackDataProvider.refresh();
sinon.assert.calledOnce(mockStackDataProvider.refresh);
Expand Down

0 comments on commit 8dacf71

Please sign in to comment.