From 2be54b2774db5ef8155df42a5ee130031dae7fc3 Mon Sep 17 00:00:00 2001 From: ErikWiens Date: Tue, 16 Jul 2024 15:41:05 -0400 Subject: [PATCH 01/45] feat: move panel observer code from DagRenderer to Panel Co-authored-by: Christopher Perkins --- src/commands/pipelines/DagRender.ts | 72 +++------------------------- src/common/panels.ts | 73 +++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 65 deletions(-) create mode 100644 src/common/panels.ts diff --git a/src/commands/pipelines/DagRender.ts b/src/commands/pipelines/DagRender.ts index d081edca..6e870f13 100644 --- a/src/commands/pipelines/DagRender.ts +++ b/src/commands/pipelines/DagRender.ts @@ -20,6 +20,7 @@ import { LSClient } from '../../services/LSClient'; import { ServerStatus } from '../../types/ServerInfoTypes'; import { JsonObject } from '../../views/panel/panelView/PanelTreeItem'; import { PanelDataProvider } from '../../views/panel/panelView/PanelDataProvider'; +import Panels from '../../common/panels'; const ROOT_PATH = ['resources', 'dag-view']; const CSS_FILE = 'dag.css'; @@ -28,7 +29,6 @@ const ICONS_DIRECTORY = '/resources/dag-view/icons/'; export default class DagRenderer { private static instance: DagRenderer | undefined; - private openPanels: { [id: string]: vscode.WebviewPanel }; private createSVGWindow: Function = () => {}; private iconSvgs: { [name: string]: string } = {}; private root: vscode.Uri; @@ -37,7 +37,6 @@ export default class DagRenderer { constructor(context: vscode.ExtensionContext) { DagRenderer.instance = this; - this.openPanels = {}; this.root = vscode.Uri.joinPath(context.extensionUri, ...ROOT_PATH); this.javaScript = vscode.Uri.joinPath(this.root, JS_FILE); this.css = vscode.Uri.joinPath(this.root, CSS_FILE); @@ -60,7 +59,6 @@ export default class DagRenderer { */ public deactivate(): void { DagRenderer.instance = undefined; - Object.values(this.openPanels).forEach(panel => panel.dispose()); } /** @@ -69,30 +67,21 @@ export default class DagRenderer { * @returns */ public async createView(node: PipelineTreeItem) { - const existingPanel = this.getDagPanel(node.id); + const p = Panels.getInstance(); + const existingPanel = p.getPanel(node.id); if (existingPanel) { existingPanel.reveal(); return; } - const panel = vscode.window.createWebviewPanel( - `DAG-${node.id}`, - node.label as string, - vscode.ViewColumn.One, - { - enableScripts: true, - localResourceRoots: [this.root], - } - ); - - panel.webview.html = this.getLoadingContent(); + const panel = p.createPanel(node.id, node.label as string, { + enableScripts: true, + localResourceRoots: [this.root], + }); panel.webview.onDidReceiveMessage(this.createMessageHandler(panel, node)); this.renderDag(panel, node); - - // To track which DAGs are currently open - this.registerDagPanel(node.id, panel); } private createMessageHandler( @@ -240,22 +229,6 @@ export default class DagRenderer { }); } - private deregisterDagPanel(runId: string) { - delete this.openPanels[runId]; - } - - private getDagPanel(runId: string): vscode.WebviewPanel | undefined { - return this.openPanels[runId]; - } - - private registerDagPanel(runId: string, panel: vscode.WebviewPanel) { - this.openPanels[runId] = panel; - - panel.onDidDispose(() => { - this.deregisterDagPanel(runId); - }, null); - } - private layoutDag(dagData: PipelineRunDag): Dagre.graphlib.Graph { const { nodes, edges } = dagData; const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); @@ -350,37 +323,6 @@ export default class DagRenderer { return canvas.svg(); } - private getLoadingContent(): string { - return ` - - - - - - - Loading - - - -
- -`; - } - private getWebviewContent({ svg, cssUri, diff --git a/src/common/panels.ts b/src/common/panels.ts new file mode 100644 index 00000000..e4ce30a9 --- /dev/null +++ b/src/common/panels.ts @@ -0,0 +1,73 @@ +import * as vscode from 'vscode'; + +export default class Panels { + private static instance: Panels | undefined; + private openPanels: { [id: string]: vscode.WebviewPanel }; + + constructor() { + this.openPanels = {}; + } + + public static getInstance(): Panels { + if (Panels.instance === undefined) { + Panels.instance = new Panels(); + } + return Panels.instance; + } + + public createPanel( + id: string, + label: string, + options?: vscode.WebviewPanelOptions & vscode.WebviewOptions + ) { + const panel = vscode.window.createWebviewPanel(id, label, vscode.ViewColumn.One, options); + panel.webview.html = this.getLoadingContent(); + + this.openPanels[id] = panel; + + panel.onDidDispose(() => { + this.deregisterPanel(id); + }, null); + + return panel; + } + + private deregisterPanel(id: string) { + delete this.openPanels[id]; + } + + public getPanel(id: string): vscode.WebviewPanel | undefined { + return this.openPanels[id]; + } + + private getLoadingContent(): string { + return ` + + + + + + + Loading + + + +
+ +`; + } +} From 78128c9d2e0bf0d85fd7f2c22a1e3aaa261b3bd0 Mon Sep 17 00:00:00 2001 From: ErikWiens Date: Wed, 17 Jul 2024 13:58:32 -0400 Subject: [PATCH 02/45] feat: add panel opens when click on plus icon to create a new stack Co-authored-by: Christopher Perkins --- bundled/tool/lsp_zenml.py | 46 +++++++--- bundled/tool/zenml_wrappers.py | 156 +++++++++++++++++++++++---------- package.json | 15 +++- src/commands/stack/cmds.ts | 12 +++ src/commands/stack/registry.ts | 4 + 5 files changed, 173 insertions(+), 60 deletions(-) diff --git a/bundled/tool/lsp_zenml.py b/bundled/tool/lsp_zenml.py index f8f4e3fe..5a20164b 100644 --- a/bundled/tool/lsp_zenml.py +++ b/bundled/tool/lsp_zenml.py @@ -32,7 +32,9 @@ from zen_watcher import ZenConfigWatcher from zenml_client import ZenMLClient -zenml_init_error = {"error": "ZenML is not initialized. Please check ZenML version requirements."} +zenml_init_error = { + "error": "ZenML is not initialized. Please check ZenML version requirements." +} class ZenLanguageServer(LanguageServer): @@ -58,7 +60,9 @@ async def is_zenml_installed(self) -> bool: if process.returncode == 0: self.show_message_log("✅ ZenML installation check: Successful.") return True - self.show_message_log("❌ ZenML installation check failed.", lsp.MessageType.Error) + self.show_message_log( + "❌ ZenML installation check failed.", lsp.MessageType.Error + ) return False except Exception as e: self.show_message_log( @@ -93,7 +97,9 @@ async def initialize_zenml_client(self): # initialize watcher self.initialize_global_config_watcher() except Exception as e: - self.notify_user(f"Failed to initialize ZenML client: {str(e)}", lsp.MessageType.Error) + self.notify_user( + f"Failed to initialize ZenML client: {str(e)}", lsp.MessageType.Error + ) def initialize_global_config_watcher(self): """Sets up and starts the Global Configuration Watcher.""" @@ -133,7 +139,9 @@ def wrapper(*args, **kwargs): with suppress_stdout_temporarily(): if wrapper_name: - wrapper_instance = getattr(self.zenml_client, wrapper_name, None) + wrapper_instance = getattr( + self.zenml_client, wrapper_name, None + ) if not wrapper_instance: return {"error": f"Wrapper '{wrapper_name}' not found."} return func(wrapper_instance, *args, **kwargs) @@ -177,25 +185,33 @@ def _construct_version_validation_response(self, meets_requirement, version_str) def send_custom_notification(self, method: str, args: dict): """Sends a custom notification to the LSP client.""" - self.show_message_log(f"Sending custom notification: {method} with args: {args}") + self.show_message_log( + f"Sending custom notification: {method} with args: {args}" + ) self.send_notification(method, args) def update_python_interpreter(self, interpreter_path): """Updates the Python interpreter path and handles errors.""" try: self.python_interpreter = interpreter_path - self.show_message_log(f"LSP_Python_Interpreter Updated: {self.python_interpreter}") + self.show_message_log( + f"LSP_Python_Interpreter Updated: {self.python_interpreter}" + ) # pylint: disable=broad-exception-caught except Exception as e: self.show_message_log( f"Failed to update Python interpreter: {str(e)}", lsp.MessageType.Error ) - def notify_user(self, message: str, msg_type: lsp.MessageType = lsp.MessageType.Info): + def notify_user( + self, message: str, msg_type: lsp.MessageType = lsp.MessageType.Info + ): """Logs a message and also notifies the user.""" self.show_message(message, msg_type) - def log_to_output(self, message: str, msg_type: lsp.MessageType = lsp.MessageType.Log) -> None: + def log_to_output( + self, message: str, msg_type: lsp.MessageType = lsp.MessageType.Log + ) -> None: """Log to output.""" self.show_message_log(message, msg_type) @@ -273,13 +289,13 @@ def fetch_pipeline_runs(wrapper_instance, args): def delete_pipeline_run(wrapper_instance, args): """Deletes a specified ZenML pipeline run.""" return wrapper_instance.delete_pipeline_run(args) - + @self.command(f"{TOOL_MODULE_NAME}.getPipelineRun") @self.zenml_command(wrapper_name="pipeline_runs_wrapper") def get_pipeline_run(wrapper_instance, args): """Gets a specified ZenML pipeline run.""" return wrapper_instance.get_pipeline_run(args) - + @self.command(f"{TOOL_MODULE_NAME}.getPipelineRunStep") @self.zenml_command(wrapper_name="pipeline_runs_wrapper") def get_run_step(wrapper_instance, args): @@ -291,9 +307,15 @@ def get_run_step(wrapper_instance, args): def get_run_artifact(wrapper_instance, args): """Gets a specified ZenML pipeline artifact""" return wrapper_instance.get_run_artifact(args) - + @self.command(f"{TOOL_MODULE_NAME}.getPipelineRunDag") @self.zenml_command(wrapper_name="pipeline_runs_wrapper") def get_run_dag(wrapper_instance, args): """Gets graph data for a specified ZenML pipeline run""" - return wrapper_instance.get_pipeline_run_graph(args) \ No newline at end of file + return wrapper_instance.get_pipeline_run_graph(args) + + @self.command(f"{TOOL_MODULE_NAME}.listComponents") + @self.zenml_command(wrapper_name="stacks_wrapper") + def list_components(wrapper_instance, args): + """Get paginated stack components from ZenML""" + return wrapper_instance.list_components(args) diff --git a/bundled/tool/zenml_wrappers.py b/bundled/tool/zenml_wrappers.py index a119fe86..092a46a5 100644 --- a/bundled/tool/zenml_wrappers.py +++ b/bundled/tool/zenml_wrappers.py @@ -15,7 +15,12 @@ import json import pathlib from typing import Any, Tuple, Union -from type_hints import GraphResponse, ErrorResponse, RunStepResponse, RunArtifactResponse +from type_hints import ( + GraphResponse, + ErrorResponse, + RunStepResponse, + RunArtifactResponse, +) from zenml_grapher import Grapher @@ -49,7 +54,9 @@ def get_global_config_directory(self): def RestZenStoreConfiguration(self): """Returns the RestZenStoreConfiguration class for store configuration.""" # pylint: disable=not-callable - return self.lazy_import("zenml.zen_stores.rest_zen_store", "RestZenStoreConfiguration") + return self.lazy_import( + "zenml.zen_stores.rest_zen_store", "RestZenStoreConfiguration" + ) def get_global_config_directory_path(self) -> str: """Get the global configuration directory path. @@ -176,7 +183,9 @@ def get_server_info(self) -> dict: store_info = json.loads(self.gc.zen_store.get_store_info().json(indent=2)) # Handle both 'store' and 'store_configuration' depending on version store_attr_name = ( - "store_configuration" if hasattr(self.gc, "store_configuration") else "store" + "store_configuration" + if hasattr(self.gc, "store_configuration") + else "store" ) store_config = json.loads(getattr(self.gc, store_attr_name).json(indent=2)) return {"storeInfo": store_info, "storeConfig": store_config} @@ -198,7 +207,9 @@ def connect(self, args, **kwargs) -> dict: try: # pylint: disable=not-callable access_token = self.web_login(url=url, verify_ssl=verify_ssl) - self._config_wrapper.set_store_configuration(remote_url=url, access_token=access_token) + self._config_wrapper.set_store_configuration( + remote_url=url, access_token=access_token + ) return {"message": "Connected successfully.", "access_token": access_token} except self.AuthorizationException as e: return {"error": f"Authorization failed: {str(e)}"} @@ -214,7 +225,9 @@ def disconnect(self, args) -> dict: try: # Adjust for changes from 'store' to 'store_configuration' store_attr_name = ( - "store_configuration" if hasattr(self.gc, "store_configuration") else "store" + "store_configuration" + if hasattr(self.gc, "store_configuration") + else "store" ) url = getattr(self.gc, store_attr_name).url store_type = self.BaseZenStore.get_store_type(url) @@ -283,15 +296,21 @@ def fetch_pipeline_runs(self, args): "version": run.body.pipeline.body.version, "stackName": run.body.stack.name, "startTime": ( - run.metadata.start_time.isoformat() if run.metadata.start_time else None + run.metadata.start_time.isoformat() + if run.metadata.start_time + else None ), "endTime": ( - run.metadata.end_time.isoformat() if run.metadata.end_time else None + run.metadata.end_time.isoformat() + if run.metadata.end_time + else None ), "os": run.metadata.client_environment.get("os", "Unknown OS"), "osVersion": run.metadata.client_environment.get( "os_version", - run.metadata.client_environment.get("mac_version", "Unknown Version"), + run.metadata.client_environment.get( + "mac_version", "Unknown Version" + ), ), "pythonVersion": run.metadata.client_environment.get( "python_version", "Unknown" @@ -326,10 +345,10 @@ def delete_pipeline_run(self, args) -> dict: return {"message": f"Pipeline run `{run_id}` deleted successfully."} except self.ZenMLBaseException as e: return {"error": f"Failed to delete pipeline run: {str(e)}"} - + def get_pipeline_run(self, args: Tuple[str]) -> dict: """Gets a ZenML pipeline run. - + Args: args (list): List of arguments. Returns: @@ -340,33 +359,39 @@ def get_pipeline_run(self, args: Tuple[str]) -> dict: run = self.client.get_pipeline_run(run_id, hydrate=True) run_data = { "id": str(run.id), - "name": run.body.pipeline.name, - "status": run.body.status, - "version": run.body.pipeline.body.version, - "stackName": run.body.stack.name, - "startTime": ( - run.metadata.start_time.isoformat() if run.metadata.start_time else None - ), - "endTime": ( - run.metadata.end_time.isoformat() if run.metadata.end_time else None - ), - "os": run.metadata.client_environment.get("os", "Unknown OS"), - "osVersion": run.metadata.client_environment.get( - "os_version", - run.metadata.client_environment.get("mac_version", "Unknown Version"), - ), - "pythonVersion": run.metadata.client_environment.get( - "python_version", "Unknown" + "name": run.body.pipeline.name, + "status": run.body.status, + "version": run.body.pipeline.body.version, + "stackName": run.body.stack.name, + "startTime": ( + run.metadata.start_time.isoformat() + if run.metadata.start_time + else None + ), + "endTime": ( + run.metadata.end_time.isoformat() if run.metadata.end_time else None + ), + "os": run.metadata.client_environment.get("os", "Unknown OS"), + "osVersion": run.metadata.client_environment.get( + "os_version", + run.metadata.client_environment.get( + "mac_version", "Unknown Version" ), + ), + "pythonVersion": run.metadata.client_environment.get( + "python_version", "Unknown" + ), } return run_data except self.ZenMLBaseException as e: return {"error": f"Failed to retrieve pipeline run: {str(e)}"} - - def get_pipeline_run_graph(self, args: Tuple[str]) -> Union[GraphResponse, ErrorResponse]: + + def get_pipeline_run_graph( + self, args: Tuple[str] + ) -> Union[GraphResponse, ErrorResponse]: """Gets a ZenML pipeline run step DAG. - + Args: args (list): List of arguments. Returns: @@ -384,7 +409,7 @@ def get_pipeline_run_graph(self, args: Tuple[str]) -> Union[GraphResponse, Error def get_run_step(self, args: Tuple[str]) -> Union[RunStepResponse, ErrorResponse]: """Gets a ZenML pipeline run step. - + Args: args (list): List of arguments. Returns: @@ -393,7 +418,9 @@ def get_run_step(self, args: Tuple[str]) -> Union[RunStepResponse, ErrorResponse try: step_run_id = args[0] step = self.client.get_run_step(step_run_id, hydrate=True) - run = self.client.get_pipeline_run(step.metadata.pipeline_run_id, hydrate=True) + run = self.client.get_pipeline_run( + step.metadata.pipeline_run_id, hydrate=True + ) step_data = { "name": step.name, @@ -404,18 +431,22 @@ def get_run_step(self, args: Tuple[str]) -> Union[RunStepResponse, ErrorResponse "email": step.body.user.name, }, "startTime": ( - step.metadata.start_time.isoformat() if step.metadata.start_time else None + step.metadata.start_time.isoformat() + if step.metadata.start_time + else None ), "endTime": ( - step.metadata.end_time.isoformat() if step.metadata.end_time else None + step.metadata.end_time.isoformat() + if step.metadata.end_time + else None ), "duration": ( - str(step.metadata.end_time - step.metadata.start_time) if step.metadata.end_time and step.metadata.start_time else None + str(step.metadata.end_time - step.metadata.start_time) + if step.metadata.end_time and step.metadata.start_time + else None ), "stackName": run.body.stack.name, - "orchestrator": { - "runId": str(run.metadata.orchestrator_run_id) - }, + "orchestrator": {"runId": str(run.metadata.orchestrator_run_id)}, "pipeline": { "name": run.body.pipeline.name, "status": run.body.status, @@ -423,15 +454,17 @@ def get_run_step(self, args: Tuple[str]) -> Union[RunStepResponse, ErrorResponse }, "cacheKey": step.metadata.cache_key, "sourceCode": step.metadata.source_code, - "logsUri": step.metadata.logs.body.uri + "logsUri": step.metadata.logs.body.uri, } return step_data except self.ZenMLBaseException as e: return {"error": f"Failed to retrieve pipeline run step: {str(e)}"} - - def get_run_artifact(self, args: Tuple[str]) -> Union[RunArtifactResponse, ErrorResponse]: + + def get_run_artifact( + self, args: Tuple[str] + ) -> Union[RunArtifactResponse, ErrorResponse]: """Gets a ZenML pipeline run artifact. - + Args: args (list): List of arguments. Returns: @@ -509,7 +542,9 @@ def fetch_stacks(self, args): return {"error": "Insufficient arguments provided."} page, max_size = args try: - stacks_page = self.client.list_stacks(page=page, size=max_size, hydrate=True) + stacks_page = self.client.list_stacks( + page=page, size=max_size, hydrate=True + ) stacks_data = self.process_stacks(stacks_page.items) return { @@ -622,17 +657,23 @@ def copy_stack(self, args) -> dict: target_stack_name = args[1] if not source_stack_name_or_id or not target_stack_name: - return {"error": "Both source stack name/id and target stack name are required"} + return { + "error": "Both source stack name/id and target stack name are required" + } try: - stack_to_copy = self.client.get_stack(name_id_or_prefix=source_stack_name_or_id) + stack_to_copy = self.client.get_stack( + name_id_or_prefix=source_stack_name_or_id + ) component_mapping = { c_type: [c.id for c in components][0] for c_type, components in stack_to_copy.components.items() if components } - self.client.create_stack(name=target_stack_name, components=component_mapping) + self.client.create_stack( + name=target_stack_name, components=component_mapping + ) return { "message": ( f"Stack `{source_stack_name_or_id}` successfully copied " @@ -644,3 +685,26 @@ def copy_stack(self, args) -> dict: self.StackComponentValidationError, ) as e: return {"error": str(e)} + + def list_components(self, args) -> dict: + page = args[0] + components = self.client.list_stack_components(page=page, hydrate=True) + return { + "index": components.index, + "max_size": components.max_size, + "total_pages": components.total_pages, + "total": components.total, + "items": [ + { + "id": str(item.id), + "name": item.name, + "flavor": item.body.flavor, + "type": item.body.type, + } + for item in components.items + ], + } + + # index, max_size, total_pages, total + # items [] + # id, name, body.flavor, body.type diff --git a/package.json b/package.json index 28c6b816..85e06b03 100644 --- a/package.json +++ b/package.json @@ -254,6 +254,12 @@ "title": "Restart LSP Server", "icon": "$(debug-restart)", "category": "ZenML Environment" + }, + { + "command": "zenml.createStack", + "title": "Create Stack", + "icon": "$(add)", + "category": "ZenML Stacks" } ], "viewsContainers": { @@ -321,14 +327,19 @@ }, { "when": "stackCommandsRegistered && view == zenmlStackView", - "command": "zenml.setStackItemsPerPage", + "command": "zenml.createStack", "group": "navigation@1" }, { "when": "stackCommandsRegistered && view == zenmlStackView", - "command": "zenml.refreshStackView", + "command": "zenml.setStackItemsPerPage", "group": "navigation@2" }, + { + "when": "stackCommandsRegistered && view == zenmlStackView", + "command": "zenml.refreshStackView", + "group": "navigation@3" + }, { "when": "pipelineCommandsRegistered && view == zenmlPipelineView", "command": "zenml.setPipelineRunsPerPage", diff --git a/src/commands/stack/cmds.ts b/src/commands/stack/cmds.ts index 9525824a..5c7b42e5 100644 --- a/src/commands/stack/cmds.ts +++ b/src/commands/stack/cmds.ts @@ -16,6 +16,8 @@ import ZenMLStatusBar from '../../views/statusBar'; import { getStackDashboardUrl, switchActiveStack } from './utils'; import { LSClient } from '../../services/LSClient'; import { showInformationMessage } from '../../utils/notifications'; +import Panels from '../../common/panels'; +import { randomUUID } from 'crypto'; /** * Refreshes the stack view. @@ -157,6 +159,15 @@ const setActiveStack = async (node: StackTreeItem): Promise => { ); }; +const createStack = async () => { + console.log('Creating a stack!'); + const id = 'stack form'; + const label = 'Create Stack'; + Panels.getInstance().createPanel(id, label); + const obj = await LSClient.getInstance().sendLsClientRequest('listComponents', [1]); + console.log(obj); +}; + /** * Opens the selected stack in the ZenML Dashboard in the browser * @@ -185,4 +196,5 @@ export const stackCommands = { copyStack, setActiveStack, goToStackUrl, + createStack, }; diff --git a/src/commands/stack/registry.ts b/src/commands/stack/registry.ts index e8973ccd..b6a4e38a 100644 --- a/src/commands/stack/registry.ts +++ b/src/commands/stack/registry.ts @@ -35,6 +35,10 @@ export const registerStackCommands = (context: ExtensionContext) => { 'zenml.refreshActiveStack', async () => await stackCommands.refreshActiveStack() ), + registerCommand( + 'zenml.createStack', + async () => await stackCommands.createStack() + ), registerCommand( 'zenml.renameStack', async (node: StackTreeItem) => await stackCommands.renameStack(node) From e42f90ce0e820f591691b3d855278e029a77b3dc Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 16 Jul 2024 21:29:00 +0200 Subject: [PATCH 03/45] add install script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 85e06b03..db80e31a 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "deploy": "vsce publish", "format": "prettier --ignore-path .gitignore --write \"**/*.+(ts|json)\"", "format-check": "prettier --ignore-path .gitignore --check \"**/*.+(ts|json)\"", + "install": "pip install -r requirements.txt --target bundled/libs", "lint": "eslint src --ext ts", "package": "webpack --mode production --devtool source-map --config ./webpack.config.js", "pretest": "npm run compile-tests && npm run compile && npm run lint", From ddc2cc4ea5c6c8b8a55b924d5cc2c45f79fdfab4 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 16 Jul 2024 21:31:13 +0200 Subject: [PATCH 04/45] Bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb9989b2..2ae648ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "zenml-vscode", - "version": "0.0.6", + "version": "0.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zenml-vscode", - "version": "0.0.6", + "version": "0.0.7", "license": "Apache-2.0", "dependencies": { "@svgdotjs/svg.js": "^3.2.4", diff --git a/package.json b/package.json index db80e31a..3c1e272c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publisher": "ZenML", "displayName": "ZenML Studio", "description": "Integrates ZenML directly into VS Code, enhancing machine learning workflow with support for pipelines, stacks, and server management.", - "version": "0.0.6", + "version": "0.0.7", "icon": "resources/extension-logo.png", "preview": true, "license": "Apache-2.0", From 962ad9101213d81d187709bb8e21abdc41597884 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Tue, 16 Jul 2024 21:42:27 +0200 Subject: [PATCH 05/45] Bump and remove preview setting --- package-lock.json | 4 ++-- package.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ae648ac..d38f7047 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "zenml-vscode", - "version": "0.0.7", + "version": "0.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zenml-vscode", - "version": "0.0.7", + "version": "0.0.8", "license": "Apache-2.0", "dependencies": { "@svgdotjs/svg.js": "^3.2.4", diff --git a/package.json b/package.json index 3c1e272c..74bf3ef6 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,10 @@ "name": "zenml-vscode", "publisher": "ZenML", "displayName": "ZenML Studio", - "description": "Integrates ZenML directly into VS Code, enhancing machine learning workflow with support for pipelines, stacks, and server management.", - "version": "0.0.7", + "description": "Integrates ZenML directly into VS Code, enhancing machine learning workflow with support for pipelines, stacks, server management and DAG visualization.", + "version": "0.0.8", "icon": "resources/extension-logo.png", - "preview": true, + "preview": false, "license": "Apache-2.0", "categories": [ "Machine Learning", From 749b35f742cc271efca447ec2ad87a330fc71b46 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Wed, 17 Jul 2024 12:25:12 -0400 Subject: [PATCH 06/45] fix: added dag-packed.js.map to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6e24fde5..51484850 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ build/ *.tsbuildinfo .history/ dag-packed.js +dag-packed.js.map # env .env From 87d38feef8e85160b6595e30768b81700a27364f Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Wed, 17 Jul 2024 12:26:16 -0400 Subject: [PATCH 07/45] fix: handled error when trying to stop a LS that is not running while trying to restart it --- src/common/server.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/common/server.ts b/src/common/server.ts index e0e0dfe8..b2862f8f 100644 --- a/src/common/server.ts +++ b/src/common/server.ts @@ -111,7 +111,11 @@ export async function restartServer( const lsClient = lsClientInstance.getLanguageClient(); if (lsClient) { traceInfo(`Server: Stop requested`); - await lsClient.stop(); + try { + await lsClient.stop(); + } catch (e) { + traceInfo(`Server: Stop failed - ${e}`, '\nContinuing to attempt to start'); + } _disposables.forEach(d => d.dispose()); _disposables = []; } From eecef5fc2434ca515b05f41f8e64e55186c52686 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Wed, 17 Jul 2024 21:44:52 +0200 Subject: [PATCH 08/45] bump version --- package-lock.json | 5 +++-- package.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d38f7047..a6a8db10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { "name": "zenml-vscode", - "version": "0.0.8", + "version": "0.0.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zenml-vscode", - "version": "0.0.8", + "version": "0.0.9", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@svgdotjs/svg.js": "^3.2.4", diff --git a/package.json b/package.json index 74bf3ef6..f604d4e9 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publisher": "ZenML", "displayName": "ZenML Studio", "description": "Integrates ZenML directly into VS Code, enhancing machine learning workflow with support for pipelines, stacks, server management and DAG visualization.", - "version": "0.0.8", + "version": "0.0.9", "icon": "resources/extension-logo.png", "preview": false, "license": "Apache-2.0", From 59e990901a66a9343e272edde598d23ce363bed0 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Wed, 17 Jul 2024 17:36:36 -0400 Subject: [PATCH 09/45] fix: removed use of pydantic json() call from get_server_info due to data no longer being compatible --- bundled/tool/type_hints.py | 22 +++++++++++++++++++++- bundled/tool/zenml_wrappers.py | 27 +++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/bundled/tool/type_hints.py b/bundled/tool/type_hints.py index bda4b20e..9b7b4404 100644 --- a/bundled/tool/type_hints.py +++ b/bundled/tool/type_hints.py @@ -55,4 +55,24 @@ class RunArtifactResponse(TypedDict): author: Dict[str, str] update: str data: Dict[str, str] - metadata: Dict[str, Any] \ No newline at end of file + metadata: Dict[str, Any] + +class ZenmlStoreInfo(TypedDict): + id: str + version: str + debug: bool + deployment_type: str + database_type: str + secrets_store_type: str + auth_scheme: str + server_url: str + dashboard_url: str + +class ZenmlStoreConfig(TypedDict): + type: str + url: str + api_token: Union[str, None] + +class ZenmlServerInfoResp(TypedDict): + store_info: ZenmlStoreInfo + store_config: ZenmlStoreConfig \ No newline at end of file diff --git a/bundled/tool/zenml_wrappers.py b/bundled/tool/zenml_wrappers.py index 092a46a5..731c7cad 100644 --- a/bundled/tool/zenml_wrappers.py +++ b/bundled/tool/zenml_wrappers.py @@ -174,21 +174,40 @@ def get_active_deployment(self): """Returns the function to get the active ZenML server deployment.""" return self.lazy_import("zenml.zen_server.utils", "get_active_deployment") - def get_server_info(self) -> dict: + def get_server_info(self) -> ZenmlServerInfoResp: """Fetches the ZenML server info. Returns: dict: Dictionary containing server info. """ - store_info = json.loads(self.gc.zen_store.get_store_info().json(indent=2)) + store_info = self.gc.zen_store.get_store_info() + # Handle both 'store' and 'store_configuration' depending on version store_attr_name = ( "store_configuration" if hasattr(self.gc, "store_configuration") else "store" ) - store_config = json.loads(getattr(self.gc, store_attr_name).json(indent=2)) - return {"storeInfo": store_info, "storeConfig": store_config} + store_config = getattr(self.gc, store_attr_name) + + return { + "storeInfo": { + "id": str(store_info.id), + "version": store_info.version, + "debug": store_info.debug, + "deployment_type": store_info.deployment_type, + "database_type": store_info.database_type, + "secrets_store_type": store_info.secrets_store_type, + "auth_scheme": store_info.auth_scheme, + "server_url": store_info.server_url, + "dashboard_url": store_info.dashboard_url, + }, + "storeConfig": { + "type": store_config.type, + "url": store_config.url, + "api_token": store_config.api_token if "api_token" in store_config else None + } + } def connect(self, args, **kwargs) -> dict: """Connects to a ZenML server. From 8e6127e7b093e5341a422e4bad810ee8120fdcbc Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Wed, 17 Jul 2024 17:57:28 -0400 Subject: [PATCH 10/45] fix: Updated getGlobalConfig to not use pydantic json() call like getServerInfo - problematic call no longer occurs --- bundled/tool/type_hints.py | 11 ++++++++++- bundled/tool/zenml_wrappers.py | 35 ++++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/bundled/tool/type_hints.py b/bundled/tool/type_hints.py index 9b7b4404..f5441944 100644 --- a/bundled/tool/type_hints.py +++ b/bundled/tool/type_hints.py @@ -75,4 +75,13 @@ class ZenmlStoreConfig(TypedDict): class ZenmlServerInfoResp(TypedDict): store_info: ZenmlStoreInfo - store_config: ZenmlStoreConfig \ No newline at end of file + store_config: ZenmlStoreConfig + +class ZenmlGlobalConfigResp(TypedDict): + user_id: str + user_email: str + analytics_opt_in: bool + version: str + active_stack_id: str + active_workspace_name: str + store: ZenmlStoreConfig \ No newline at end of file diff --git a/bundled/tool/zenml_wrappers.py b/bundled/tool/zenml_wrappers.py index 731c7cad..acbe0b32 100644 --- a/bundled/tool/zenml_wrappers.py +++ b/bundled/tool/zenml_wrappers.py @@ -12,15 +12,9 @@ # permissions and limitations under the License. """This module provides wrappers for ZenML configuration and operations.""" -import json import pathlib from typing import Any, Tuple, Union -from type_hints import ( - GraphResponse, - ErrorResponse, - RunStepResponse, - RunArtifactResponse, -) +from type_hints import GraphResponse, ErrorResponse, RunStepResponse, RunArtifactResponse, ZenmlServerInfoResp, ZenmlGlobalConfigResp from zenml_grapher import Grapher @@ -106,19 +100,32 @@ def set_store_configuration(self, remote_url: str, access_token: str): ) self.gc.set_store(new_store_config) - def get_global_configuration(self) -> dict: + def get_global_configuration(self) -> ZenmlGlobalConfigResp: """Get the global configuration. Returns: dict: Global configuration. """ - gc_dict = json.loads(self.gc.json(indent=2)) - user_id = gc_dict.get("user_id", "") - if user_id and user_id.startswith("UUID('") and user_id.endswith("')"): - gc_dict["user_id"] = user_id[6:-2] + store_attr_name = ( + "store_configuration" if hasattr(self.gc, "store_configuration") else "store" + ) + + store_data = getattr(self.gc, store_attr_name) - return gc_dict + return { + "user_id": str(self.gc.user_id), + "user_email": self.gc.user_email, + "analytics_opt_in": self.gc.analytics_opt_in, + "version": self.gc.version, + "active_stack_id": str(self.gc.active_stack_id), + "active_workspace_name": self.gc.active_workspace_name, + "store": { + "type": store_data.type, + "url": store_data.url, + "api_token": store_data.api_token if hasattr(store_data, "api_token") else None + } + } class ZenServerWrapper: @@ -205,7 +212,7 @@ def get_server_info(self) -> ZenmlServerInfoResp: "storeConfig": { "type": store_config.type, "url": store_config.url, - "api_token": store_config.api_token if "api_token" in store_config else None + "api_token": store_config.api_token if hasattr(store_config, "api_token") else None } } From 70109cc733f0a207b36765dd704e4080b06c0a26 Mon Sep 17 00:00:00 2001 From: Alex Strick van Linschoten Date: Thu, 18 Jul 2024 10:50:39 +0200 Subject: [PATCH 11/45] Bump version to 0.0.10 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6a8db10..8960e019 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "zenml-vscode", - "version": "0.0.9", + "version": "0.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zenml-vscode", - "version": "0.0.9", + "version": "0.0.10", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index f604d4e9..bffa2ec5 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publisher": "ZenML", "displayName": "ZenML Studio", "description": "Integrates ZenML directly into VS Code, enhancing machine learning workflow with support for pipelines, stacks, server management and DAG visualization.", - "version": "0.0.9", + "version": "0.0.10", "icon": "resources/extension-logo.png", "preview": false, "license": "Apache-2.0", From f019708c2fa793b4527959b91a68366b9e565fc8 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Thu, 18 Jul 2024 15:11:53 -0400 Subject: [PATCH 12/45] feat: Added Stack Components view to activity bar --- bundled/tool/type_hints.py | 18 +- bundled/tool/zenml_wrappers.py | 58 +++-- package.json | 5 + src/commands/stack/cmds.ts | 2 +- src/services/ZenExtension.ts | 2 + src/types/StackTypes.ts | 15 +- .../activityBar/common/LoadingTreeItem.ts | 1 + .../componentView/ComponentDataProvider.ts | 230 ++++++++++++++++++ .../activityBar/stackView/StackTreeItems.ts | 8 +- 9 files changed, 312 insertions(+), 27 deletions(-) create mode 100644 src/views/activityBar/componentView/ComponentDataProvider.ts diff --git a/bundled/tool/type_hints.py b/bundled/tool/type_hints.py index f5441944..8f7b2357 100644 --- a/bundled/tool/type_hints.py +++ b/bundled/tool/type_hints.py @@ -1,7 +1,6 @@ from typing import Any, TypedDict, Dict, List, Union from uuid import UUID - class StepArtifactBody(TypedDict): type: str artifact: Dict[str, str] @@ -20,8 +19,6 @@ class GraphEdge(TypedDict): source: str target: str - - class GraphResponse(TypedDict): nodes: List[GraphNode] edges: List[GraphEdge] @@ -84,4 +81,17 @@ class ZenmlGlobalConfigResp(TypedDict): version: str active_stack_id: str active_workspace_name: str - store: ZenmlStoreConfig \ No newline at end of file + store: ZenmlStoreConfig + +class ComponentResponse(TypedDict): + id: str + name: str + flavor: str + type: str + +class ListComponentsResponse(TypedDict): + index: int + max_size: int + total_pages: int + total: int + items: List[ComponentResponse] \ No newline at end of file diff --git a/bundled/tool/zenml_wrappers.py b/bundled/tool/zenml_wrappers.py index acbe0b32..fe77394c 100644 --- a/bundled/tool/zenml_wrappers.py +++ b/bundled/tool/zenml_wrappers.py @@ -14,8 +14,16 @@ import pathlib from typing import Any, Tuple, Union -from type_hints import GraphResponse, ErrorResponse, RunStepResponse, RunArtifactResponse, ZenmlServerInfoResp, ZenmlGlobalConfigResp from zenml_grapher import Grapher +from type_hints import ( + GraphResponse, + ErrorResponse, + RunStepResponse, + RunArtifactResponse, + ZenmlServerInfoResp, + ZenmlGlobalConfigResp, + ListComponentsResponse, +) class GlobalConfigWrapper: @@ -712,24 +720,38 @@ def copy_stack(self, args) -> dict: ) as e: return {"error": str(e)} - def list_components(self, args) -> dict: + def list_components(self, args: Tuple[int, int, Union[str, None]]) -> Union[ListComponentsResponse,ErrorResponse]: + if len(args) < 2: + return {"error": "Insufficient arguments provided."} + page = args[0] - components = self.client.list_stack_components(page=page, hydrate=True) - return { - "index": components.index, - "max_size": components.max_size, - "total_pages": components.total_pages, - "total": components.total, - "items": [ - { - "id": str(item.id), - "name": item.name, - "flavor": item.body.flavor, - "type": item.body.type, - } - for item in components.items - ], - } + max_size = args[1] + filter = None + + if len(args) >= 3: + filter = args[2] + + try: + components = self.client.list_stack_components(page=page, size=max_size, type=filter, hydrate=True) + + return { + "index": components.index, + "max_size": components.max_size, + "total_pages": components.total_pages, + "total": components.total, + "items": [ + { + "id": str(item.id), + "name": item.name, + "flavor": item.body.flavor, + "type": item.body.type, + } + for item in components.items + ], + } + except self.ZenMLBaseException as e: + return {"error": f"Failed to retrieve list of stack components: {str(e)}"} + # index, max_size, total_pages, total # items [] diff --git a/package.json b/package.json index bffa2ec5..4a9d1f91 100644 --- a/package.json +++ b/package.json @@ -291,6 +291,11 @@ "name": "Stacks", "icon": "$(layers)" }, + { + "id": "zenmlComponentView", + "name": "Stack Components", + "icon": "$(extensions)" + }, { "id": "zenmlPipelineView", "name": "Pipeline Runs", diff --git a/src/commands/stack/cmds.ts b/src/commands/stack/cmds.ts index 5c7b42e5..9f2a4b4d 100644 --- a/src/commands/stack/cmds.ts +++ b/src/commands/stack/cmds.ts @@ -164,7 +164,7 @@ const createStack = async () => { const id = 'stack form'; const label = 'Create Stack'; Panels.getInstance().createPanel(id, label); - const obj = await LSClient.getInstance().sendLsClientRequest('listComponents', [1]); + const obj = await LSClient.getInstance().sendLsClientRequest('listComponents', [1, 10]); console.log(obj); }; diff --git a/src/services/ZenExtension.ts b/src/services/ZenExtension.ts index 3836cabf..3905598b 100644 --- a/src/services/ZenExtension.ts +++ b/src/services/ZenExtension.ts @@ -41,6 +41,7 @@ import ZenMLStatusBar from '../views/statusBar'; import { LSClient } from './LSClient'; import { toggleCommands } from '../utils/global'; import { PanelDataProvider } from '../views/panel/panelView/PanelDataProvider'; +import { ComponentDataProvider } from '../views/activityBar/componentView/ComponentDataProvider'; export interface IServerInfo { name: string; @@ -61,6 +62,7 @@ export class ZenExtension { private static dataProviders = new Map>([ ['zenmlServerView', ServerDataProvider.getInstance()], ['zenmlStackView', StackDataProvider.getInstance()], + ['zenmlComponentView', ComponentDataProvider.getInstance()], ['zenmlPipelineView', PipelineDataProvider.getInstance()], ['zenmlPanelView', PanelDataProvider.getInstance()], ]); diff --git a/src/types/StackTypes.ts b/src/types/StackTypes.ts index 5ada4087..d8666320 100644 --- a/src/types/StackTypes.ts +++ b/src/types/StackTypes.ts @@ -44,4 +44,17 @@ interface StackComponent { export type StacksResponse = StacksData | ErrorMessageResponse | VersionMismatchError; -export { Stack, Components, StackComponent, StacksData }; +interface ComponentsListData { + index: number; + max_size: number; + total_pages: number; + total: number; + items: Array; +} + +export type ComponentsListResponse = + | ComponentsListData + | ErrorMessageResponse + | VersionMismatchError; + +export { Stack, Components, StackComponent, StacksData, ComponentsListData }; diff --git a/src/views/activityBar/common/LoadingTreeItem.ts b/src/views/activityBar/common/LoadingTreeItem.ts index 9e0fe9b6..df52fdb5 100644 --- a/src/views/activityBar/common/LoadingTreeItem.ts +++ b/src/views/activityBar/common/LoadingTreeItem.ts @@ -23,6 +23,7 @@ export class LoadingTreeItem extends TreeItem { export const LOADING_TREE_ITEMS = new Map([ ['server', new LoadingTreeItem('Refreshing Server View...')], ['stacks', new LoadingTreeItem('Refreshing Stacks View...')], + ['components', new LoadingTreeItem('Refreshing Components View...')], ['pipelineRuns', new LoadingTreeItem('Refreshing Pipeline Runs...')], ['environment', new LoadingTreeItem('Refreshing Environments...')], ['lsClient', new LoadingTreeItem('Waiting for Language Server to start...', '')], diff --git a/src/views/activityBar/componentView/ComponentDataProvider.ts b/src/views/activityBar/componentView/ComponentDataProvider.ts new file mode 100644 index 00000000..537c6f22 --- /dev/null +++ b/src/views/activityBar/componentView/ComponentDataProvider.ts @@ -0,0 +1,230 @@ +// 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 { Event, EventEmitter, TreeDataProvider, TreeItem, window, workspace } from 'vscode'; +import { State } from 'vscode-languageclient'; +import { EventBus } from '../../../services/EventBus'; +import { LSClient } from '../../../services/LSClient'; +import { + ITEMS_PER_PAGE_OPTIONS, + LSCLIENT_STATE_CHANGED, + LSP_ZENML_CLIENT_INITIALIZED, + LSP_ZENML_STACK_CHANGED, +} from '../../../utils/constants'; +import { ErrorTreeItem, createErrorItem, createAuthErrorItem } from '../common/ErrorTreeItem'; +import { LOADING_TREE_ITEMS } from '../common/LoadingTreeItem'; +import { CommandTreeItem } from '../common/PaginationTreeItems'; +import { ComponentsListResponse, StackComponent } from '../../../types/StackTypes'; +import { StackComponentTreeItem } from '../stackView/StackTreeItems'; + +export class ComponentDataProvider implements TreeDataProvider { + private _onDidChangeTreeData = new EventEmitter(); + readonly onDidChangeTreeData: Event = + this._onDidChangeTreeData.event; + + private static instance: ComponentDataProvider | null = null; + private eventBus = EventBus.getInstance(); + private zenmlClientReady = false; + public components: TreeItem[] = [LOADING_TREE_ITEMS.get('components')!]; + + private pagination = { + currentPage: 1, + itemsPerPage: 10, + totalItems: 0, + totalPages: 0, + }; + + constructor() { + this.subscribeToEvents(); + } + + /** + * Subscribes to relevant events to trigger a refresh of the tree view. + */ + public subscribeToEvents(): void { + this.eventBus.on(LSCLIENT_STATE_CHANGED, (newState: State) => { + if (newState === State.Running) { + this.refresh(); + } else { + this.components = [LOADING_TREE_ITEMS.get('lsClient')!]; + this._onDidChangeTreeData.fire(undefined); + } + }); + + this.eventBus.on(LSP_ZENML_CLIENT_INITIALIZED, (isInitialized: boolean) => { + this.zenmlClientReady = isInitialized; + + if (!isInitialized) { + this.components = [LOADING_TREE_ITEMS.get('stacks')!]; + this._onDidChangeTreeData.fire(undefined); + return; + } + this.refresh(); + this.eventBus.off(LSP_ZENML_STACK_CHANGED, () => this.refresh()); + this.eventBus.on(LSP_ZENML_STACK_CHANGED, () => this.refresh()); + }); + } + + /** + * Retrieves the singleton instance of ComponentDataProvider + * + * @returns {ComponentDataProvider} The signleton instance. + */ + public static getInstance(): ComponentDataProvider { + if (!ComponentDataProvider.instance) { + ComponentDataProvider.instance = new ComponentDataProvider(); + } + + return ComponentDataProvider.instance; + } + + /** + * Returns the provided tree item. + * + * @param element element The tree item to return. + * @returns The corresponding VS Code tree item + */ + public getTreeItem(element: TreeItem): TreeItem { + return element; + } + + public async refresh(): Promise { + this.components = [LOADING_TREE_ITEMS.get('components')!]; + this._onDidChangeTreeData.fire(undefined); + + const page = this.pagination.currentPage; + const itemsPerPage = this.pagination.itemsPerPage; + + try { + const newComponentsData = await this.fetchComponents(page, itemsPerPage); + this.components = newComponentsData; + } catch (e) { + this.components = createErrorItem(e); + } + + this._onDidChangeTreeData.fire(undefined); + } + + public async goToNextPage() { + if (this.pagination.currentPage < this.pagination.totalPages) { + this.pagination.currentPage++; + await this.refresh(); + } + } + + public async goToPreviousPage() { + if (this.pagination.currentPage > 1) { + this.pagination.currentPage--; + await this.refresh(); + } + } + + public async updateItemsPerPage() { + const selected = await window.showQuickPick(ITEMS_PER_PAGE_OPTIONS, { + placeHolder: 'Choose the max number of stacks to display per page', + }); + if (selected) { + this.pagination.itemsPerPage = parseInt(selected, 10); + this.pagination.currentPage = 1; + await this.refresh(); + } + } + + public async getChildren(element?: TreeItem): Promise { + if (!element) { + if (Array.isArray(this.components) && this.components.length > 0) { + return this.components; + } + + const components = await this.fetchComponents( + this.pagination.currentPage, + this.pagination.itemsPerPage + ); + return components; + } + + return undefined; + } + + private async fetchComponents(page: number = 1, itemsPerPage: number = 10) { + if (!this.zenmlClientReady) { + return [LOADING_TREE_ITEMS.get('zenmlClient')!]; + } + + try { + const lsClient = LSClient.getInstance(); + const result = await lsClient.sendLsClientRequest('listComponents', [ + page, + itemsPerPage, + ]); + + if (Array.isArray(result) && result.length === 1 && 'error' in result[0]) { + const errorMessage = result[0].error; + if (errorMessage.includes('Authentication error')) { + return createAuthErrorItem(errorMessage); + } + } + + if (!result || 'error' in result) { + if ('clientVersion' in result && 'serverVersion' in result) { + return createErrorItem(result); + } else { + console.error(`Failed to fetch stack components: ${result.error}`); + return []; + } + } + + if ('items' in result) { + const { items, total, total_pages, index, max_size } = result; + this.pagination = { + currentPage: index, + itemsPerPage: max_size, + totalItems: total, + totalPages: total_pages, + }; + + const components = items.map( + (component: StackComponent) => new StackComponentTreeItem(component) + ); + return this.addPaginationCommands(components); + } else { + console.error('Unexpected response format:', result); + return []; + } + } catch (e: any) { + console.error(`Failed to fetch components: ${e}`); + return [ + new ErrorTreeItem('Error', `Failed to fetch components: ${e.message || e.toString()}`), + ]; + } + } + + private addPaginationCommands(treeItems: TreeItem[]): TreeItem[] { + if (this.pagination.currentPage < this.pagination.totalPages) { + treeItems.push( + new CommandTreeItem('Next Page', 'zenml.nextComponentPage', undefined, 'arrow-circle-right') + ); + } + + if (this.pagination.currentPage > 1) { + treeItems.unshift( + new CommandTreeItem( + 'Previous Page', + 'zenml.previousComponentPage', + undefined, + 'arrow-circle-left' + ) + ); + } + return treeItems; + } +} diff --git a/src/views/activityBar/stackView/StackTreeItems.ts b/src/views/activityBar/stackView/StackTreeItems.ts index 524e8993..ea54c802 100644 --- a/src/views/activityBar/stackView/StackTreeItems.ts +++ b/src/views/activityBar/stackView/StackTreeItems.ts @@ -44,13 +44,15 @@ export class StackTreeItem extends vscode.TreeItem { export class StackComponentTreeItem extends vscode.TreeItem { constructor( public component: StackComponent, - public stackId: string + public stackId?: string ) { super(component.name, vscode.TreeItemCollapsibleState.None); - this.tooltip = `Type: ${component.type}, Flavor: ${component.flavor}, ID: ${stackId}`; + this.tooltip = stackId + ? `Type: ${component.type}, Flavor: ${component.flavor}, ID: ${stackId}` + : `Type: ${component.type}, Flavor: ${component.flavor}`; this.description = `${component.type} (${component.flavor})`; this.contextValue = 'stackComponent'; - this.id = `${stackId}-${component.id}`; + this.id = stackId ? `${stackId}-${component.id}` : `${component.id}`; } } From 47cc7bdca28a445042e0d16aa34f03d2b2146720 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Thu, 18 Jul 2024 15:43:20 -0400 Subject: [PATCH 13/45] feat: Added Refresh and ItemsPerPage commands to Components View --- package.json | 22 +++++++++ src/commands/components/cmds.ts | 36 ++++++++++++++ src/commands/components/registry.ts | 48 +++++++++++++++++++ src/services/ZenExtension.ts | 2 + src/utils/global.ts | 1 + .../componentView/ComponentDataProvider.ts | 2 +- 6 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/commands/components/cmds.ts create mode 100644 src/commands/components/registry.ts diff --git a/package.json b/package.json index 4a9d1f91..ecacab1b 100644 --- a/package.json +++ b/package.json @@ -196,6 +196,18 @@ "icon": "$(check)", "category": "ZenML Stacks" }, + { + "command": "zenml.setComponentItemsPerPage", + "title": "Set Components Per Page", + "icon": "$(layers)", + "category": "ZenML Components" + }, + { + "command": "zenml.refreshComponentView", + "title": "Refresh Component View", + "icon": "$(refresh)", + "category": "ZenML Components" + }, { "command": "zenml.copyStack", "title": "Copy Stack", @@ -346,6 +358,16 @@ "command": "zenml.refreshStackView", "group": "navigation@3" }, + { + "when": "componentCommandsRegistered && view == zenmlComponentView", + "command": "zenml.setComponentItemsPerPage", + "group": "navigation@1" + }, + { + "when": "componentCommandsRegistered && view == zenmlComponentView", + "command": "zenml.refreshComponentView", + "group": "navigation@2" + }, { "when": "pipelineCommandsRegistered && view == zenmlPipelineView", "command": "zenml.setPipelineRunsPerPage", diff --git a/src/commands/components/cmds.ts b/src/commands/components/cmds.ts new file mode 100644 index 00000000..8e33eb3d --- /dev/null +++ b/src/commands/components/cmds.ts @@ -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 * as vscode from 'vscode'; + +import ZenMLStatusBar from '../../views/statusBar'; +import { LSClient } from '../../services/LSClient'; +import { showInformationMessage } from '../../utils/notifications'; +import Panels from '../../common/panels'; +import { ComponentDataProvider } from '../../views/activityBar/componentView/ComponentDataProvider'; + +const refreshComponentView = async () => { + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Refreshing Component View...', + cancellable: false, + }, + async progress => { + await ComponentDataProvider.getInstance().refresh(); + } + ); +}; + +export const componentCommands = { + refreshComponentView, +}; diff --git a/src/commands/components/registry.ts b/src/commands/components/registry.ts new file mode 100644 index 00000000..eac8e31a --- /dev/null +++ b/src/commands/components/registry.ts @@ -0,0 +1,48 @@ +// 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 { componentCommands } from './cmds'; +import { registerCommand } from '../../common/vscodeapi'; +import { ZenExtension } from '../../services/ZenExtension'; +import { ExtensionContext, commands } from 'vscode'; +import { ComponentDataProvider } from '../../views/activityBar/componentView/ComponentDataProvider'; + +/** + * Registers stack component-related commands for the extension. + * + * @param {ExtensionContext} context - The context in which the extension operates, used for registering commands and managing their lifecycle. + */ +export const registerComponentCommands = (context: ExtensionContext) => { + const componentDataProvider = ComponentDataProvider.getInstance(); + try { + const registeredCommands = [ + registerCommand( + 'zenml.setComponentItemsPerPage', + async () => await componentDataProvider.updateItemsPerPage() + ), + registerCommand( + 'zenml.refreshComponentView', + async () => await componentCommands.refreshComponentView() + ), + ]; + + registeredCommands.forEach(cmd => { + context.subscriptions.push(cmd); + ZenExtension.commandDisposables.push(cmd); + }); + + commands.executeCommand('setContext', 'componentCommandsRegistered', true); + } catch (e) { + console.error('Error registering component commands:', e); + commands.executeCommand('setContext', 'componentCommandsRegistered', false); + } +}; diff --git a/src/services/ZenExtension.ts b/src/services/ZenExtension.ts index 3905598b..43a0f8e7 100644 --- a/src/services/ZenExtension.ts +++ b/src/services/ZenExtension.ts @@ -42,6 +42,7 @@ import { LSClient } from './LSClient'; import { toggleCommands } from '../utils/global'; import { PanelDataProvider } from '../views/panel/panelView/PanelDataProvider'; import { ComponentDataProvider } from '../views/activityBar/componentView/ComponentDataProvider'; +import { registerComponentCommands } from '../commands/components/registry'; export interface IServerInfo { name: string; @@ -70,6 +71,7 @@ export class ZenExtension { private static registries = [ registerServerCommands, registerStackCommands, + registerComponentCommands, registerPipelineCommands, ]; diff --git a/src/utils/global.ts b/src/utils/global.ts index 8645a153..f14700f5 100644 --- a/src/utils/global.ts +++ b/src/utils/global.ts @@ -122,6 +122,7 @@ export function getDefaultPythonInterpreterPath(): string { * @param state The state to set the commands to. */ export async function toggleCommands(state: boolean): Promise { + await vscode.commands.executeCommand('setContext', 'stackCommandsRegistered', state); await vscode.commands.executeCommand('setContext', 'stackCommandsRegistered', state); await vscode.commands.executeCommand('setContext', 'serverCommandsRegistered', state); await vscode.commands.executeCommand('setContext', 'pipelineCommandsRegistered', state); diff --git a/src/views/activityBar/componentView/ComponentDataProvider.ts b/src/views/activityBar/componentView/ComponentDataProvider.ts index 537c6f22..7b109092 100644 --- a/src/views/activityBar/componentView/ComponentDataProvider.ts +++ b/src/views/activityBar/componentView/ComponentDataProvider.ts @@ -64,7 +64,7 @@ export class ComponentDataProvider implements TreeDataProvider { this.zenmlClientReady = isInitialized; if (!isInitialized) { - this.components = [LOADING_TREE_ITEMS.get('stacks')!]; + this.components = [LOADING_TREE_ITEMS.get('components')!]; this._onDidChangeTreeData.fire(undefined); return; } From ed472095158ff80d1bea5c89246589ffb2416992 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Thu, 18 Jul 2024 16:24:19 -0400 Subject: [PATCH 14/45] chore: Refactored Pagination in DataProviders to base class --- src/commands/components/registry.ts | 8 ++ src/utils/global.ts | 2 +- .../common/PaginatedDataProvider.ts | 111 ++++++++++++++++++ .../componentView/ComponentDataProvider.ts | 103 ++-------------- .../pipelineView/PipelineDataProvider.ts | 111 ++---------------- .../stackView/StackDataProvider.ts | 101 ++-------------- src/views/statusBar/index.ts | 10 +- 7 files changed, 159 insertions(+), 287 deletions(-) create mode 100644 src/views/activityBar/common/PaginatedDataProvider.ts diff --git a/src/commands/components/registry.ts b/src/commands/components/registry.ts index eac8e31a..392e04fa 100644 --- a/src/commands/components/registry.ts +++ b/src/commands/components/registry.ts @@ -33,6 +33,14 @@ export const registerComponentCommands = (context: ExtensionContext) => { 'zenml.refreshComponentView', async () => await componentCommands.refreshComponentView() ), + registerCommand( + 'zenml.nextComponentPage', + async () => await componentDataProvider.goToNextPage() + ), + registerCommand( + 'zenml.previousComponentPage', + async () => await componentDataProvider.goToPreviousPage() + ), ]; registeredCommands.forEach(cmd => { diff --git a/src/utils/global.ts b/src/utils/global.ts index f14700f5..a4f94a3b 100644 --- a/src/utils/global.ts +++ b/src/utils/global.ts @@ -123,7 +123,7 @@ export function getDefaultPythonInterpreterPath(): string { */ export async function toggleCommands(state: boolean): Promise { await vscode.commands.executeCommand('setContext', 'stackCommandsRegistered', state); - await vscode.commands.executeCommand('setContext', 'stackCommandsRegistered', state); + await vscode.commands.executeCommand('setContext', 'componentCommandsRegistered', state); await vscode.commands.executeCommand('setContext', 'serverCommandsRegistered', state); await vscode.commands.executeCommand('setContext', 'pipelineCommandsRegistered', state); await vscode.commands.executeCommand('setContext', 'environmentCommandsRegistered', state); diff --git a/src/views/activityBar/common/PaginatedDataProvider.ts b/src/views/activityBar/common/PaginatedDataProvider.ts new file mode 100644 index 00000000..7d61a36f --- /dev/null +++ b/src/views/activityBar/common/PaginatedDataProvider.ts @@ -0,0 +1,111 @@ +// 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 { Event, EventEmitter, TreeDataProvider, TreeItem, window } from 'vscode'; +import { ITEMS_PER_PAGE_OPTIONS } from '../../../utils/constants'; +import { CommandTreeItem } from './PaginationTreeItems'; +import { LoadingTreeItem } from './LoadingTreeItem'; +import { ErrorTreeItem } from './ErrorTreeItem'; + +export class PaginatedDataProvider implements TreeDataProvider { + protected _onDidChangeTreeData = new EventEmitter(); + readonly onDidChangeTreeData: Event = + this._onDidChangeTreeData.event; + protected pagination = { + currentPage: 1, + itemsPerPage: 10, + totalItems: 0, + totalPages: 0, + }; + public items: TreeItem[] = []; + protected viewName: string = ''; + + public async goToNextPage() { + if (this.pagination.currentPage < this.pagination.totalPages) { + this.pagination.currentPage++; + await this.refresh(); + } + } + + public async goToPreviousPage() { + if (this.pagination.currentPage > 1) { + this.pagination.currentPage--; + await this.refresh(); + } + } + + public async updateItemsPerPage() { + const selected = await window.showQuickPick(ITEMS_PER_PAGE_OPTIONS, { + placeHolder: 'Choose the max number of items to display per page', + }); + if (selected) { + this.pagination.itemsPerPage = parseInt(selected, 10); + this.pagination.currentPage = 1; + await this.refresh(); + } + } + + public async refresh(): Promise { + this._onDidChangeTreeData.fire(undefined); + } + + /** + * Returns the provided tree item. + * + * @param element element The tree item to return. + * @returns The corresponding VS Code tree item + */ + public getTreeItem(element: TreeItem): TreeItem { + return element; + } + + public async getChildren(element?: TreeItem): Promise { + if (!element) { + if (this.items[0] instanceof LoadingTreeItem || this.items[0] instanceof ErrorTreeItem) { + return this.items; + } + return this.addPaginationCommands(this.items.slice()); + } + + if ('children' in element && Array.isArray(element.children)) { + return element.children; + } + + return undefined; + } + + private addPaginationCommands(treeItems: TreeItem[]): TreeItem[] { + if (this.pagination.currentPage < this.pagination.totalPages) { + treeItems.push( + new CommandTreeItem( + 'Next Page', + `zenml.next${this.viewName}Page`, + undefined, + 'arrow-circle-right' + ) + ); + } + + if (this.pagination.currentPage > 1) { + treeItems.unshift( + new CommandTreeItem( + 'Previous Page', + `zenml.previous${this.viewName}Page`, + undefined, + 'arrow-circle-left' + ) + ); + } + return treeItems; + } +} diff --git a/src/views/activityBar/componentView/ComponentDataProvider.ts b/src/views/activityBar/componentView/ComponentDataProvider.ts index 7b109092..9c302857 100644 --- a/src/views/activityBar/componentView/ComponentDataProvider.ts +++ b/src/views/activityBar/componentView/ComponentDataProvider.ts @@ -10,12 +10,10 @@ // 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 { Event, EventEmitter, TreeDataProvider, TreeItem, window, workspace } from 'vscode'; import { State } from 'vscode-languageclient'; import { EventBus } from '../../../services/EventBus'; import { LSClient } from '../../../services/LSClient'; import { - ITEMS_PER_PAGE_OPTIONS, LSCLIENT_STATE_CHANGED, LSP_ZENML_CLIENT_INITIALIZED, LSP_ZENML_STACK_CHANGED, @@ -25,26 +23,18 @@ import { LOADING_TREE_ITEMS } from '../common/LoadingTreeItem'; import { CommandTreeItem } from '../common/PaginationTreeItems'; import { ComponentsListResponse, StackComponent } from '../../../types/StackTypes'; import { StackComponentTreeItem } from '../stackView/StackTreeItems'; +import { PaginatedDataProvider } from '../common/PaginatedDataProvider'; -export class ComponentDataProvider implements TreeDataProvider { - private _onDidChangeTreeData = new EventEmitter(); - readonly onDidChangeTreeData: Event = - this._onDidChangeTreeData.event; - +export class ComponentDataProvider extends PaginatedDataProvider { private static instance: ComponentDataProvider | null = null; private eventBus = EventBus.getInstance(); private zenmlClientReady = false; - public components: TreeItem[] = [LOADING_TREE_ITEMS.get('components')!]; - - private pagination = { - currentPage: 1, - itemsPerPage: 10, - totalItems: 0, - totalPages: 0, - }; constructor() { + super(); this.subscribeToEvents(); + this.items = [LOADING_TREE_ITEMS.get('components')!]; + this.viewName = 'Component'; } /** @@ -55,7 +45,7 @@ export class ComponentDataProvider implements TreeDataProvider { if (newState === State.Running) { this.refresh(); } else { - this.components = [LOADING_TREE_ITEMS.get('lsClient')!]; + this.items = [LOADING_TREE_ITEMS.get('lsClient')!]; this._onDidChangeTreeData.fire(undefined); } }); @@ -64,7 +54,7 @@ export class ComponentDataProvider implements TreeDataProvider { this.zenmlClientReady = isInitialized; if (!isInitialized) { - this.components = [LOADING_TREE_ITEMS.get('components')!]; + this.items = [LOADING_TREE_ITEMS.get('components')!]; this._onDidChangeTreeData.fire(undefined); return; } @@ -87,18 +77,8 @@ export class ComponentDataProvider implements TreeDataProvider { return ComponentDataProvider.instance; } - /** - * Returns the provided tree item. - * - * @param element element The tree item to return. - * @returns The corresponding VS Code tree item - */ - public getTreeItem(element: TreeItem): TreeItem { - return element; - } - public async refresh(): Promise { - this.components = [LOADING_TREE_ITEMS.get('components')!]; + this.items = [LOADING_TREE_ITEMS.get('components')!]; this._onDidChangeTreeData.fire(undefined); const page = this.pagination.currentPage; @@ -106,55 +86,14 @@ export class ComponentDataProvider implements TreeDataProvider { try { const newComponentsData = await this.fetchComponents(page, itemsPerPage); - this.components = newComponentsData; + this.items = newComponentsData; } catch (e) { - this.components = createErrorItem(e); + this.items = createErrorItem(e); } this._onDidChangeTreeData.fire(undefined); } - public async goToNextPage() { - if (this.pagination.currentPage < this.pagination.totalPages) { - this.pagination.currentPage++; - await this.refresh(); - } - } - - public async goToPreviousPage() { - if (this.pagination.currentPage > 1) { - this.pagination.currentPage--; - await this.refresh(); - } - } - - public async updateItemsPerPage() { - const selected = await window.showQuickPick(ITEMS_PER_PAGE_OPTIONS, { - placeHolder: 'Choose the max number of stacks to display per page', - }); - if (selected) { - this.pagination.itemsPerPage = parseInt(selected, 10); - this.pagination.currentPage = 1; - await this.refresh(); - } - } - - public async getChildren(element?: TreeItem): Promise { - if (!element) { - if (Array.isArray(this.components) && this.components.length > 0) { - return this.components; - } - - const components = await this.fetchComponents( - this.pagination.currentPage, - this.pagination.itemsPerPage - ); - return components; - } - - return undefined; - } - private async fetchComponents(page: number = 1, itemsPerPage: number = 10) { if (!this.zenmlClientReady) { return [LOADING_TREE_ITEMS.get('zenmlClient')!]; @@ -195,7 +134,7 @@ export class ComponentDataProvider implements TreeDataProvider { const components = items.map( (component: StackComponent) => new StackComponentTreeItem(component) ); - return this.addPaginationCommands(components); + return components; } else { console.error('Unexpected response format:', result); return []; @@ -207,24 +146,4 @@ export class ComponentDataProvider implements TreeDataProvider { ]; } } - - private addPaginationCommands(treeItems: TreeItem[]): TreeItem[] { - if (this.pagination.currentPage < this.pagination.totalPages) { - treeItems.push( - new CommandTreeItem('Next Page', 'zenml.nextComponentPage', undefined, 'arrow-circle-right') - ); - } - - if (this.pagination.currentPage > 1) { - treeItems.unshift( - new CommandTreeItem( - 'Previous Page', - 'zenml.previousComponentPage', - undefined, - 'arrow-circle-left' - ) - ); - } - return treeItems; - } } diff --git a/src/views/activityBar/pipelineView/PipelineDataProvider.ts b/src/views/activityBar/pipelineView/PipelineDataProvider.ts index f707cc91..fc83d062 100644 --- a/src/views/activityBar/pipelineView/PipelineDataProvider.ts +++ b/src/views/activityBar/pipelineView/PipelineDataProvider.ts @@ -25,27 +25,20 @@ import { ErrorTreeItem, createErrorItem, createAuthErrorItem } from '../common/E import { LOADING_TREE_ITEMS } from '../common/LoadingTreeItem'; import { PipelineRunTreeItem, PipelineTreeItem } from './PipelineTreeItems'; import { CommandTreeItem } from '../common/PaginationTreeItems'; +import { PaginatedDataProvider } from '../common/PaginatedDataProvider'; /** * Provides data for the pipeline run tree view, displaying detailed information about each pipeline run. */ -export class PipelineDataProvider implements TreeDataProvider { - private _onDidChangeTreeData = new EventEmitter(); - readonly onDidChangeTreeData = this._onDidChangeTreeData.event; - +export class PipelineDataProvider extends PaginatedDataProvider { private static instance: PipelineDataProvider | null = null; private eventBus = EventBus.getInstance(); private zenmlClientReady = false; - private pipelineRuns: PipelineTreeItem[] | TreeItem[] = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; - - private pagination = { - currentPage: 1, - itemsPerPage: 20, - totalItems: 0, - totalPages: 0, - }; constructor() { + super(); + this.items = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; + this.viewName = 'PipelineRuns'; this.subscribeToEvents(); } @@ -57,7 +50,7 @@ export class PipelineDataProvider implements TreeDataProvider { if (newState === State.Running) { this.refresh(); } else { - this.pipelineRuns = [LOADING_TREE_ITEMS.get('lsClient')!]; + this.items = [LOADING_TREE_ITEMS.get('lsClient')!]; this._onDidChangeTreeData.fire(undefined); } }); @@ -66,7 +59,7 @@ export class PipelineDataProvider implements TreeDataProvider { this.zenmlClientReady = isInitialized; if (!isInitialized) { - this.pipelineRuns = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; + this.items = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; this._onDidChangeTreeData.fire(undefined); return; } @@ -83,10 +76,10 @@ export class PipelineDataProvider implements TreeDataProvider { * @returns {PipelineDataProvider} The singleton instance. */ public static getInstance(): PipelineDataProvider { - if (!this.instance) { - this.instance = new PipelineDataProvider(); + if (!PipelineDataProvider.instance) { + PipelineDataProvider.instance = new PipelineDataProvider(); } - return this.instance; + return PipelineDataProvider.instance; } /** @@ -95,74 +88,21 @@ export class PipelineDataProvider implements TreeDataProvider { * @returns A promise resolving to void. */ public async refresh(): Promise { - this.pipelineRuns = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; + this.items = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; this._onDidChangeTreeData.fire(undefined); const page = this.pagination.currentPage; const itemsPerPage = this.pagination.itemsPerPage; try { const newPipelineData = await this.fetchPipelineRuns(page, itemsPerPage); - this.pipelineRuns = newPipelineData; + this.items = newPipelineData; } catch (error: any) { - this.pipelineRuns = createErrorItem(error); + this.items = createErrorItem(error); } this._onDidChangeTreeData.fire(undefined); } - /** - * Retrieves the tree item for a given pipeline run. - * - * @param element The pipeline run item. - * @returns The corresponding VS Code tree item. - */ - getTreeItem(element: TreeItem): TreeItem { - return element; - } - - /** - * Retrieves the children for a given tree item. - * - * @param element The parent tree item. If undefined, root pipeline runs are fetched. - * @returns A promise resolving to an array of child tree items or undefined if there are no children. - */ - async getChildren(element?: TreeItem): Promise { - if (!element) { - if (Array.isArray(this.pipelineRuns) && this.pipelineRuns.length > 0) { - return this.pipelineRuns; - } - - // Fetch pipeline runs for the current page and add pagination controls if necessary - const runs = await this.fetchPipelineRuns( - this.pagination.currentPage, - this.pagination.itemsPerPage - ); - if (this.pagination.currentPage < this.pagination.totalPages) { - runs.push( - new CommandTreeItem( - 'Next Page', - 'zenml.nextPipelineRunsPage', - undefined, - 'arrow-circle-right' - ) - ); - } - if (this.pagination.currentPage > 1) { - runs.unshift( - new CommandTreeItem( - 'Previous Page', - 'zenml.previousPipelineRunsPage', - undefined, - 'arrow-circle-left' - ) - ); - } - return runs; - } else if (element instanceof PipelineTreeItem) { - return element.children; - } - return undefined; - } /** * Fetches pipeline runs from the server and maps them to tree items for display. * @@ -234,29 +174,4 @@ export class PipelineDataProvider implements TreeDataProvider { ]; } } - - public async goToNextPage() { - if (this.pagination.currentPage < this.pagination.totalPages) { - this.pagination.currentPage++; - await this.refresh(); - } - } - - public async goToPreviousPage() { - if (this.pagination.currentPage > 1) { - this.pagination.currentPage--; - await this.refresh(); - } - } - - public async updateItemsPerPage() { - const selected = await window.showQuickPick(ITEMS_PER_PAGE_OPTIONS, { - placeHolder: 'Choose the max number of pipeline runs to display per page', - }); - if (selected) { - this.pagination.itemsPerPage = parseInt(selected, 10); - this.pagination.currentPage = 1; - await this.refresh(); - } - } } diff --git a/src/views/activityBar/stackView/StackDataProvider.ts b/src/views/activityBar/stackView/StackDataProvider.ts index 9dfe1b76..b611767d 100644 --- a/src/views/activityBar/stackView/StackDataProvider.ts +++ b/src/views/activityBar/stackView/StackDataProvider.ts @@ -25,26 +25,18 @@ import { ErrorTreeItem, createErrorItem, createAuthErrorItem } from '../common/E import { LOADING_TREE_ITEMS } from '../common/LoadingTreeItem'; import { StackComponentTreeItem, StackTreeItem } from './StackTreeItems'; import { CommandTreeItem } from '../common/PaginationTreeItems'; +import { PaginatedDataProvider } from '../common/PaginatedDataProvider'; -export class StackDataProvider implements TreeDataProvider { - private _onDidChangeTreeData = new EventEmitter(); - readonly onDidChangeTreeData: Event = - this._onDidChangeTreeData.event; - +export class StackDataProvider extends PaginatedDataProvider { private static instance: StackDataProvider | null = null; private eventBus = EventBus.getInstance(); private zenmlClientReady = false; - public stacks: StackTreeItem[] | TreeItem[] = [LOADING_TREE_ITEMS.get('stacks')!]; - - private pagination = { - currentPage: 1, - itemsPerPage: 20, - totalItems: 0, - totalPages: 0, - }; constructor() { + super(); this.subscribeToEvents(); + this.items = [LOADING_TREE_ITEMS.get('stacks')!]; + this.viewName = 'Stack'; } /** @@ -55,7 +47,7 @@ export class StackDataProvider implements TreeDataProvider { if (newState === State.Running) { this.refresh(); } else { - this.stacks = [LOADING_TREE_ITEMS.get('lsClient')!]; + this.items = [LOADING_TREE_ITEMS.get('lsClient')!]; this._onDidChangeTreeData.fire(undefined); } }); @@ -64,7 +56,7 @@ export class StackDataProvider implements TreeDataProvider { this.zenmlClientReady = isInitialized; if (!isInitialized) { - this.stacks = [LOADING_TREE_ITEMS.get('stacks')!]; + this.items = [LOADING_TREE_ITEMS.get('stacks')!]; this._onDidChangeTreeData.fire(undefined); return; } @@ -86,23 +78,13 @@ export class StackDataProvider implements TreeDataProvider { return this.instance; } - /** - * Returns the provided tree item. - * - * @param {TreeItem} element The tree item to return. - * @returns The corresponding VS Code tree item. - */ - getTreeItem(element: TreeItem): TreeItem { - return element; - } - /** * Refreshes the tree view data by refetching stacks and triggering the onDidChangeTreeData event. * * @returns {Promise} A promise that resolves when the tree view data has been refreshed. */ public async refresh(): Promise { - this.stacks = [LOADING_TREE_ITEMS.get('stacks')!]; + this.items = [LOADING_TREE_ITEMS.get('stacks')!]; this._onDidChangeTreeData.fire(undefined); const page = this.pagination.currentPage; @@ -110,9 +92,9 @@ export class StackDataProvider implements TreeDataProvider { try { const newStacksData = await this.fetchStacksWithComponents(page, itemsPerPage); - this.stacks = newStacksData; + this.items = newStacksData; } catch (error: any) { - this.stacks = createErrorItem(error); + this.items = createErrorItem(error); } this._onDidChangeTreeData.fire(undefined); @@ -179,69 +161,6 @@ export class StackDataProvider implements TreeDataProvider { } } - public async goToNextPage() { - if (this.pagination.currentPage < this.pagination.totalPages) { - this.pagination.currentPage++; - await this.refresh(); - } - } - - public async goToPreviousPage() { - if (this.pagination.currentPage > 1) { - this.pagination.currentPage--; - await this.refresh(); - } - } - - public async updateItemsPerPage() { - const selected = await window.showQuickPick(ITEMS_PER_PAGE_OPTIONS, { - placeHolder: 'Choose the max number of stacks to display per page', - }); - if (selected) { - this.pagination.itemsPerPage = parseInt(selected, 10); - this.pagination.currentPage = 1; - await this.refresh(); - } - } - - /** - * Retrieves the children of a given tree item. - * - * @param {TreeItem} element The tree item whose children to retrieve. - * @returns A promise resolving to an array of child tree items or undefined if there are no children. - */ - async getChildren(element?: TreeItem): Promise { - if (!element) { - if (Array.isArray(this.stacks) && this.stacks.length > 0) { - return this.stacks; - } - - const stacks = await this.fetchStacksWithComponents( - this.pagination.currentPage, - this.pagination.itemsPerPage - ); - if (this.pagination.currentPage < this.pagination.totalPages) { - stacks.push( - new CommandTreeItem('Next Page', 'zenml.nextStackPage', undefined, 'arrow-circle-right') - ); - } - if (this.pagination.currentPage > 1) { - stacks.unshift( - new CommandTreeItem( - 'Previous Page', - 'zenml.previousStackPage', - undefined, - 'arrow-circle-left' - ) - ); - } - return stacks; - } else if (element instanceof StackTreeItem) { - return element.children; - } - return undefined; - } - /** * Helper method to determine if a stack is the active stack. * diff --git a/src/views/statusBar/index.ts b/src/views/statusBar/index.ts index c6db6759..d931a52e 100644 --- a/src/views/statusBar/index.ts +++ b/src/views/statusBar/index.ts @@ -116,17 +116,17 @@ export default class ZenMLStatusBar { */ private async switchStack(): Promise { const stackDataProvider = StackDataProvider.getInstance(); - const { stacks } = stackDataProvider; + const { items } = stackDataProvider; - const containsErrors = stacks.some(stack => stack instanceof ErrorTreeItem); + const containsErrors = items.some(stack => stack instanceof ErrorTreeItem); - if (containsErrors || stacks.length === 0) { + if (containsErrors || items.length === 0) { window.showErrorMessage('No stacks available.'); return; } - const activeStack = stacks.find(stack => stack.id === this.activeStackId); - const otherStacks = stacks.filter(stack => stack.id !== this.activeStackId); + const activeStack = items.find(stack => stack.id === this.activeStackId); + const otherStacks = items.filter(stack => stack.id !== this.activeStackId); const quickPickItems = [ { From 0dc748a89b2e0bb339c6f48f15ba776475795686 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Thu, 18 Jul 2024 19:45:33 -0400 Subject: [PATCH 15/45] feature: Added listFlavors command to LS --- bundled/tool/lsp_zenml.py | 24 +++++++--- bundled/tool/type_hints.py | 30 ++++++++++--- bundled/tool/zenml_wrappers.py | 44 +++++++++++++++++-- src/commands/stack/cmds.ts | 11 +++-- .../common/PaginatedDataProvider.ts | 6 ++- 5 files changed, 94 insertions(+), 21 deletions(-) diff --git a/bundled/tool/lsp_zenml.py b/bundled/tool/lsp_zenml.py index 5a20164b..bdfc4049 100644 --- a/bundled/tool/lsp_zenml.py +++ b/bundled/tool/lsp_zenml.py @@ -277,6 +277,24 @@ def rename_stack(wrapper_instance, args): def copy_stack(wrapper_instance, args): """Copies a specified ZenML stack to a new stack.""" return wrapper_instance.copy_stack(args) + + @self.command(f"{TOOL_MODULE_NAME}.listComponents") + @self.zenml_command(wrapper_name="stacks_wrapper") + def list_components(wrapper_instance, args): + """Get paginated stack components from ZenML""" + return wrapper_instance.list_components(args) + + @self.command(f"{TOOL_MODULE_NAME}.getComponentTypes") + @self.zenml_command(wrapper_name="stacks_wrapper") + def get_component_types(wrapper_instance, args): + """Get paginated stack components from ZenML""" + return wrapper_instance.get_component_types() + + @self.command(f"{TOOL_MODULE_NAME}.listFlavors") + @self.zenml_command(wrapper_name="stacks_wrapper") + def list_flavors(wrapper_instance, args): + """Get paginated stack components from ZenML""" + return wrapper_instance.list_flavors(args) @self.command(f"{TOOL_MODULE_NAME}.getPipelineRuns") @self.zenml_command(wrapper_name="pipeline_runs_wrapper") @@ -313,9 +331,3 @@ def get_run_artifact(wrapper_instance, args): def get_run_dag(wrapper_instance, args): """Gets graph data for a specified ZenML pipeline run""" return wrapper_instance.get_pipeline_run_graph(args) - - @self.command(f"{TOOL_MODULE_NAME}.listComponents") - @self.zenml_command(wrapper_name="stacks_wrapper") - def list_components(wrapper_instance, args): - """Get paginated stack components from ZenML""" - return wrapper_instance.list_components(args) diff --git a/bundled/tool/type_hints.py b/bundled/tool/type_hints.py index 8f7b2357..7d6247a6 100644 --- a/bundled/tool/type_hints.py +++ b/bundled/tool/type_hints.py @@ -1,6 +1,7 @@ -from typing import Any, TypedDict, Dict, List, Union +from typing import Any, TypedDict, Dict, List, Optional from uuid import UUID + class StepArtifactBody(TypedDict): type: str artifact: Dict[str, str] @@ -34,9 +35,9 @@ class RunStepResponse(TypedDict): id: str status: str author: Dict[str, str] - startTime: Union[str, None] - endTime: Union[str, None] - duration: Union[str, None] + startTime: Optional[str] + endTime: Optional[str] + duration: Optional[str] stackName: str orchestrator: Dict[str, str] pipeline: Dict[str, str] @@ -68,7 +69,7 @@ class ZenmlStoreInfo(TypedDict): class ZenmlStoreConfig(TypedDict): type: str url: str - api_token: Union[str, None] + api_token: Optional[str] class ZenmlServerInfoResp(TypedDict): store_info: ZenmlStoreInfo @@ -83,7 +84,7 @@ class ZenmlGlobalConfigResp(TypedDict): active_workspace_name: str store: ZenmlStoreConfig -class ComponentResponse(TypedDict): +class StackComponent(TypedDict): id: str name: str flavor: str @@ -94,4 +95,19 @@ class ListComponentsResponse(TypedDict): max_size: int total_pages: int total: int - items: List[ComponentResponse] \ No newline at end of file + items: List[StackComponent] + +class Flavor(TypedDict): + id: str + name: str + type: str + logo_url: str + description: str + config_schema: Dict[str, Any] + +class ListFlavorsResponse(TypedDict): + index: int + max_size: int + total_pages: int + total: int + items: List[StackComponent] \ No newline at end of file diff --git a/bundled/tool/zenml_wrappers.py b/bundled/tool/zenml_wrappers.py index fe77394c..11c52b18 100644 --- a/bundled/tool/zenml_wrappers.py +++ b/bundled/tool/zenml_wrappers.py @@ -13,7 +13,7 @@ """This module provides wrappers for ZenML configuration and operations.""" import pathlib -from typing import Any, Tuple, Union +from typing import Any, Tuple, Union, List, Optional from zenml_grapher import Grapher from type_hints import ( GraphResponse, @@ -23,6 +23,7 @@ ZenmlServerInfoResp, ZenmlGlobalConfigResp, ListComponentsResponse, + ListFlavorsResponse ) @@ -564,6 +565,11 @@ def IllegalOperationError(self) -> Any: def StackComponentValidationError(self): """Returns the ZenML StackComponentValidationError class.""" return self.lazy_import("zenml.exceptions", "StackComponentValidationError") + + @property + def StackComponentType(self): + """Returns the ZenML StackComponentType enum.""" + return self.lazy_import("zenml.enums", "StackComponentType") @property def ZenKeyError(self) -> Any: @@ -752,7 +758,37 @@ def list_components(self, args: Tuple[int, int, Union[str, None]]) -> Union[List except self.ZenMLBaseException as e: return {"error": f"Failed to retrieve list of stack components: {str(e)}"} + def get_component_types(self) -> List[str]: + return self.StackComponentType.values() + + def list_flavors(self, args: Tuple[int, int, Optional[str]]) -> Union[ListFlavorsResponse, ErrorResponse]: + if len(args) < 2: + return {"error": "Insufficient arguments provided."} + + page = args[0] + max_size = args[1] + filter = None + if len(args) >= 3: + filter = args[2] - # index, max_size, total_pages, total - # items [] - # id, name, body.flavor, body.type + try: + flavors = self.client.list_flavors(page=page, size=max_size, type=filter, hydrate=True) + + return { + "index": flavors.index, + "max_size": flavors.max_size, + "total_pages": flavors.total_pages, + "total": flavors.total, + "items": [ + { + "id": str(flavor.id), + "name": flavor.name, + "type": flavor.body.type, + "logo_url": flavor.body.logo_url, + "config_schema": flavor.metadata.config_schema, + } for flavor in flavors.items + ] + } + + except self.ZenMLBaseException as e: + return {"error": f"Failed to retrieve list of flavors: {str(e)}"} \ No newline at end of file diff --git a/src/commands/stack/cmds.ts b/src/commands/stack/cmds.ts index 9f2a4b4d..b71b3acd 100644 --- a/src/commands/stack/cmds.ts +++ b/src/commands/stack/cmds.ts @@ -161,10 +161,15 @@ const setActiveStack = async (node: StackTreeItem): Promise => { const createStack = async () => { console.log('Creating a stack!'); - const id = 'stack form'; + const id = 'stack-form'; const label = 'Create Stack'; - Panels.getInstance().createPanel(id, label); - const obj = await LSClient.getInstance().sendLsClientRequest('listComponents', [1, 10]); + // Panels.getInstance().createPanel(id, label); + // const obj = await LSClient.getInstance().sendLsClientRequest('listComponents', [1, 10]); + const obj = await LSClient.getInstance().sendLsClientRequest('listFlavors', [ + 1, + 10000, + 'orchestrator', + ]); console.log(obj); }; diff --git a/src/views/activityBar/common/PaginatedDataProvider.ts b/src/views/activityBar/common/PaginatedDataProvider.ts index 7d61a36f..50baea8c 100644 --- a/src/views/activityBar/common/PaginatedDataProvider.ts +++ b/src/views/activityBar/common/PaginatedDataProvider.ts @@ -71,7 +71,11 @@ export class PaginatedDataProvider implements TreeDataProvider { public async getChildren(element?: TreeItem): Promise { if (!element) { - if (this.items[0] instanceof LoadingTreeItem || this.items[0] instanceof ErrorTreeItem) { + if ( + this.items.length === 0 || + this.items[0] instanceof LoadingTreeItem || + this.items[0] instanceof ErrorTreeItem + ) { return this.items; } return this.addPaginationCommands(this.items.slice()); From b8c172bf026ce5566454542fa5788af936dffdfd Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Thu, 18 Jul 2024 20:29:18 -0400 Subject: [PATCH 16/45] chore: Moved extension context functionality to new base class for DagRenderer, so I can use it across other inheritted classes --- src/commands/pipelines/DagRender.ts | 22 ++++++++++++++++------ src/common/WebviewBase.ts | 21 +++++++++++++++++++++ src/extension.ts | 3 ++- 3 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 src/common/WebviewBase.ts diff --git a/src/commands/pipelines/DagRender.ts b/src/commands/pipelines/DagRender.ts index 6e870f13..93038818 100644 --- a/src/commands/pipelines/DagRender.ts +++ b/src/commands/pipelines/DagRender.ts @@ -21,13 +21,14 @@ import { ServerStatus } from '../../types/ServerInfoTypes'; import { JsonObject } from '../../views/panel/panelView/PanelTreeItem'; import { PanelDataProvider } from '../../views/panel/panelView/PanelDataProvider'; import Panels from '../../common/panels'; +import WebviewBase from '../../common/WebviewBase'; const ROOT_PATH = ['resources', 'dag-view']; const CSS_FILE = 'dag.css'; const JS_FILE = 'dag-packed.js'; const ICONS_DIRECTORY = '/resources/dag-view/icons/'; -export default class DagRenderer { +export default class DagRenderer extends WebviewBase { private static instance: DagRenderer | undefined; private createSVGWindow: Function = () => {}; private iconSvgs: { [name: string]: string } = {}; @@ -35,14 +36,19 @@ export default class DagRenderer { private javaScript: vscode.Uri; private css: vscode.Uri; - constructor(context: vscode.ExtensionContext) { - DagRenderer.instance = this; - this.root = vscode.Uri.joinPath(context.extensionUri, ...ROOT_PATH); + constructor() { + super(); + + if (WebviewBase.context === null) { + throw new Error('Extension Context Not Propagated'); + } + + this.root = vscode.Uri.joinPath(WebviewBase.context.extensionUri, ...ROOT_PATH); this.javaScript = vscode.Uri.joinPath(this.root, JS_FILE); this.css = vscode.Uri.joinPath(this.root, CSS_FILE); this.loadSvgWindowLib(); - this.loadIcons(context.extensionPath + ICONS_DIRECTORY); + this.loadIcons(WebviewBase.context.extensionPath + ICONS_DIRECTORY); } /** @@ -50,7 +56,11 @@ export default class DagRenderer { * * @returns {DagRenderer | undefined} The singleton instance if it exists */ - public static getInstance(): DagRenderer | undefined { + public static getInstance(): DagRenderer { + if (!DagRenderer.instance) { + DagRenderer.instance = new DagRenderer(); + } + return DagRenderer.instance; } diff --git a/src/common/WebviewBase.ts b/src/common/WebviewBase.ts new file mode 100644 index 00000000..acc43a80 --- /dev/null +++ b/src/common/WebviewBase.ts @@ -0,0 +1,21 @@ +// 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 * as vscode from 'vscode'; + +export default class WebviewBase { + protected static context: vscode.ExtensionContext | null = null; + + public static setContext(context: vscode.ExtensionContext) { + WebviewBase.context = context; + } +} diff --git a/src/extension.ts b/src/extension.ts index 45abea59..8bc37721 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -20,6 +20,7 @@ import { registerEnvironmentCommands } from './commands/environment/registry'; import { LSP_ZENML_CLIENT_INITIALIZED } from './utils/constants'; import { toggleCommands } from './utils/global'; import DagRenderer from './commands/pipelines/DagRender'; +import WebviewBase from './common/WebviewBase'; export async function activate(context: vscode.ExtensionContext) { const eventBus = EventBus.getInstance(); @@ -48,7 +49,7 @@ export async function activate(context: vscode.ExtensionContext) { }) ); - new DagRenderer(context); + WebviewBase.setContext(context); } /** From c6034f0783ed4ef7992f60adca3ff87dcd53a5e0 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Thu, 18 Jul 2024 22:57:03 -0400 Subject: [PATCH 17/45] feat: Stack Form is now being rendered --- package-lock.json | 95 +++++++++++++++++++- package.json | 2 + resources/stacks-form/stacks.css | 79 +++++++++++++++++ resources/stacks-form/stacks.js | 0 src/commands/stack/StackForm.ts | 146 +++++++++++++++++++++++++++++++ src/commands/stack/cmds.ts | 15 +--- src/common/api.ts | 68 ++++++++++++++ src/types/StackTypes.ts | 21 ++++- 8 files changed, 409 insertions(+), 17 deletions(-) create mode 100644 resources/stacks-form/stacks.css create mode 100644 resources/stacks-form/stacks.js create mode 100644 src/commands/stack/StackForm.ts create mode 100644 src/common/api.ts diff --git a/package-lock.json b/package-lock.json index 8960e019..df46f7e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "axios": "^1.6.7", "dagre": "^0.8.5", "fs-extra": "^11.2.0", + "hbs": "^4.2.0", "svg-pan-zoom": "github:bumbu/svg-pan-zoom", "svgdom": "^0.1.19", "vscode-languageclient": "^9.0.1" @@ -22,6 +23,7 @@ "devDependencies": { "@types/dagre": "^0.7.52", "@types/fs-extra": "^11.0.4", + "@types/hbs": "^4.0.4", "@types/mocha": "^10.0.6", "@types/node": "^18.19.18", "@types/sinon": "^17.0.3", @@ -514,6 +516,16 @@ "@types/node": "*" } }, + "node_modules/@types/hbs": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/hbs/-/hbs-4.0.4.tgz", + "integrity": "sha512-GH3SIb2tzDBnTByUSOIVcD6AcLufnydBllTuFAIAGMhqPNbz8GL4tLryVdNqhq0NQEb5mVpu2FJOrUeqwJrPtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "handlebars": "^4.1.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2579,6 +2591,12 @@ "unicode-trie": "^2.0.0" } }, + "node_modules/foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==", + "license": "Apache2" + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -2798,6 +2816,36 @@ "lodash": "^4.17.15" } }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -2855,6 +2903,20 @@ "node": ">= 0.4" } }, + "node_modules/hbs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hbs/-/hbs-4.2.0.tgz", + "integrity": "sha512-dQwHnrfWlTk5PvG9+a45GYpg0VpX47ryKF8dULVd6DtwOE6TEcYQXQ5QM6nyOx/h7v3bvEQbdn19EDAcfUAgZg==", + "license": "MIT", + "dependencies": { + "handlebars": "4.7.7", + "walk": "2.3.15" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -3645,8 +3707,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "optional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3929,8 +3989,7 @@ "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "node_modules/nise": { "version": "5.1.9", @@ -5481,6 +5540,19 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "dev": true }, + "node_modules/uglify-js": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.0.tgz", + "integrity": "sha512-wNKHUY2hYYkf6oSFfhwwiHo4WCHzHmzcXsqXYTN9ja3iApYIFbb2U6ics9hBcYLHcYGQoAlwnZlTrf3oF+BL/Q==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/underscore": { "version": "1.13.6", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", @@ -5627,6 +5699,15 @@ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, + "node_modules/walk": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.15.tgz", + "integrity": "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "foreachasync": "^3.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", @@ -5807,6 +5888,12 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, "node_modules/workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", diff --git a/package.json b/package.json index ecacab1b..9193efa1 100644 --- a/package.json +++ b/package.json @@ -431,6 +431,7 @@ "devDependencies": { "@types/dagre": "^0.7.52", "@types/fs-extra": "^11.0.4", + "@types/hbs": "^4.0.4", "@types/mocha": "^10.0.6", "@types/node": "^18.19.18", "@types/sinon": "^17.0.3", @@ -458,6 +459,7 @@ "axios": "^1.6.7", "dagre": "^0.8.5", "fs-extra": "^11.2.0", + "hbs": "^4.2.0", "svg-pan-zoom": "github:bumbu/svg-pan-zoom", "svgdom": "^0.1.19", "vscode-languageclient": "^9.0.1" diff --git a/resources/stacks-form/stacks.css b/resources/stacks-form/stacks.css new file mode 100644 index 00000000..870fd0ac --- /dev/null +++ b/resources/stacks-form/stacks.css @@ -0,0 +1,79 @@ +h2 { + text-align: center; +} + +input { + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); +} + +input[type='radio'] { + appearance: none; + width: 15px; + height: 15px; + border-radius: 50%; + background-clip: content-box; + border: 2px solid var(--vscode-editor-foreground); + background-color: var(--vscode-editor-background); +} + +input[type='radio']:checked { + background-color: var(--vscode-editor-foreground); + padding: 2px; +} + +p { + margin: 0; + padding: 0; +} + +.options { + display: flex; + flex-wrap: nowrap; + overflow-x: auto; + align-items: center; + width: 100%; + border: 2px var(--vscode-editor-foreground) solid; + border-radius: 5px; + scrollbar-color: var(--vscode-editor-foreground) var(--vscode-editor-background); +} + +.single-option { + display: flex; + flex-direction: row; + align-items: start; + justify-content: center; + padding: 5px; + margin: 10px; + flex-shrink: 0; +} + +.single-option input { + margin-right: 5px; +} + +.single-option label { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: #eee; + color: #111; + text-align: center; + padding: 5px; + border: 2px var(--vscode-editor-foreground) solid; + border-radius: 5px; +} + +.single-option img { + width: 50px; + height: 50px; + flex-shrink: 0; +} + +.center { + display: flex; + justify-content: center; + align-items: center; + margin: 10px; +} diff --git a/resources/stacks-form/stacks.js b/resources/stacks-form/stacks.js new file mode 100644 index 00000000..e69de29b diff --git a/src/commands/stack/StackForm.ts b/src/commands/stack/StackForm.ts new file mode 100644 index 00000000..4b188ef6 --- /dev/null +++ b/src/commands/stack/StackForm.ts @@ -0,0 +1,146 @@ +// 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 * as vscode from 'vscode'; +import WebviewBase from '../../common/WebviewBase'; +import { handlebars } from 'hbs'; +import Panels from '../../common/panels'; +import { getAllFlavors, getAllStackComponents } from '../../common/api'; +import { Flavor, StackComponent } from '../../types/StackTypes'; + +type MixedComponent = { name: string; id: string; url: string }; + +const ROOT_PATH = ['resources', 'stacks-form']; +const CSS_FILE = 'stacks.css'; +const JS_FILE = 'stacks.js'; + +export default class StackForm extends WebviewBase { + private static instance: StackForm | null = null; + + private root: vscode.Uri; + private javaScript: vscode.Uri; + private css: vscode.Uri; + private template: HandlebarsTemplateDelegate; + + public static getInstance(): StackForm { + if (!StackForm.instance) { + StackForm.instance = new StackForm(); + } + + return StackForm.instance; + } + + constructor() { + super(); + + if (WebviewBase.context === null) { + throw new Error('Extension Context Not Propagated'); + } + + this.root = vscode.Uri.joinPath(WebviewBase.context.extensionUri, ...ROOT_PATH); + this.javaScript = vscode.Uri.joinPath(this.root, JS_FILE); + this.css = vscode.Uri.joinPath(this.root, CSS_FILE); + + handlebars.registerHelper('capitalize', (str: string) => { + return str + .split('_') + .map(word => word[0].toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); + }); + + this.template = handlebars.compile(this.produceTemplate()); + } + + public async display() { + const panels = Panels.getInstance(); + const existingPanel = panels.getPanel('stack-form'); + if (existingPanel) { + existingPanel.reveal(); + return; + } + + const panel = panels.createPanel('stack-form', 'Stack Form', { + enableForms: true, + enableScripts: true, + retainContextWhenHidden: true, + }); + + await this.renderForm(panel); + } + + private async renderForm(panel: vscode.WebviewPanel) { + const flavors = await getAllFlavors(); + const components = await getAllStackComponents(); + const options = this.convertComponents(flavors, components); + const js = panel.webview.asWebviewUri(this.javaScript); + const css = panel.webview.asWebviewUri(this.css); + + panel.webview.html = this.template({ options, js, css }); + } + + private convertComponents( + flavors: Flavor[], + components: { [type: string]: StackComponent[] } + ): { [type: string]: MixedComponent[] } { + const out: { [type: string]: MixedComponent[] } = {}; + + Object.keys(components).forEach(key => { + out[key] = components[key].map(component => { + return { + name: component.name, + id: component.id, + url: + flavors.find( + flavor => flavor.type === component.type && flavor.name === component.flavor + )?.logo_url ?? '', + }; + }); + }); + + return out; + } + + private produceTemplate(): string { + return ` + + + + + + + + Stack Form + + +

Create Stack

+
+ + {{#each options}} +

{{capitalize @key}}

+
+ {{#each this}} +
+ + +
+ {{/each}} +
+ {{/each}} +
+
+ + + + `; + } +} diff --git a/src/commands/stack/cmds.ts b/src/commands/stack/cmds.ts index b71b3acd..f8f53779 100644 --- a/src/commands/stack/cmds.ts +++ b/src/commands/stack/cmds.ts @@ -18,6 +18,7 @@ import { LSClient } from '../../services/LSClient'; import { showInformationMessage } from '../../utils/notifications'; import Panels from '../../common/panels'; import { randomUUID } from 'crypto'; +import StackForm from './StackForm'; /** * Refreshes the stack view. @@ -159,18 +160,8 @@ const setActiveStack = async (node: StackTreeItem): Promise => { ); }; -const createStack = async () => { - console.log('Creating a stack!'); - const id = 'stack-form'; - const label = 'Create Stack'; - // Panels.getInstance().createPanel(id, label); - // const obj = await LSClient.getInstance().sendLsClientRequest('listComponents', [1, 10]); - const obj = await LSClient.getInstance().sendLsClientRequest('listFlavors', [ - 1, - 10000, - 'orchestrator', - ]); - console.log(obj); +const createStack = () => { + StackForm.getInstance().display(); }; /** diff --git a/src/common/api.ts b/src/common/api.ts new file mode 100644 index 00000000..64b7d492 --- /dev/null +++ b/src/common/api.ts @@ -0,0 +1,68 @@ +import { LSClient } from '../services/LSClient'; +import { + ComponentsListResponse, + Flavor, + FlavorListResponse, + StackComponent, +} from '../types/StackTypes'; + +let flavors: Flavor[] = []; + +export const getAllFlavors = async (): Promise => { + if (flavors.length > 0) { + return flavors; + } + const lsClient = LSClient.getInstance(); + + let [page, maxPage] = [0, 1]; + do { + page++; + const resp = await lsClient.sendLsClientRequest('listFlavors', [ + page, + 10000, + ]); + + if ('error' in resp) { + console.error(`Error retrieving flavors: ${resp.error.toString()}`); + throw new Error(resp.error); + } + + maxPage = resp.total_pages; + flavors = flavors.concat(resp.items); + } while (page < maxPage); + return flavors; +}; + +export const getAllStackComponents = async (): Promise<{ + [type: string]: StackComponent[]; +}> => { + const lsClient = LSClient.getInstance(); + let components: StackComponent[] = []; + let [page, maxPage] = [0, 1]; + + do { + page++; + const resp = await lsClient.sendLsClientRequest('listComponents', [ + page, + 10000, + ]); + + if ('error' in resp) { + console.error(`Error retrieving components: ${resp.error.toString()}`); + throw new Error(resp.error); + } + + maxPage = resp.total_pages; + components = components.concat(resp.items); + } while (page < maxPage); + + const out: { [type: string]: StackComponent[] } = {}; + components.forEach(component => { + if (!(component.type in out)) { + out[component.type] = []; + } + out[component.type].push(component); + }); + + return out; +}; diff --git a/src/types/StackTypes.ts b/src/types/StackTypes.ts index d8666320..930fec6a 100644 --- a/src/types/StackTypes.ts +++ b/src/types/StackTypes.ts @@ -57,4 +57,23 @@ export type ComponentsListResponse = | ErrorMessageResponse | VersionMismatchError; -export { Stack, Components, StackComponent, StacksData, ComponentsListData }; +interface Flavor { + id: string; + name: string; + type: string; + logo_url: string; + description: string; + config_schema: { [key: string]: any }; +} + +interface FlavorListData { + index: number; + max_size: number; + total_pages: number; + total: number; + items: Flavor[]; +} + +export type FlavorListResponse = FlavorListData | ErrorMessageResponse | VersionMismatchError; + +export { Stack, Components, StackComponent, StacksData, ComponentsListData, Flavor }; From 82cda5d64793cfd649772a60b39750175f8d0c0b Mon Sep 17 00:00:00 2001 From: ErikWiens Date: Tue, 16 Jul 2024 15:41:05 -0400 Subject: [PATCH 18/45] feat: move panel observer code from DagRenderer to Panel Co-authored-by: Christopher Perkins --- src/commands/pipelines/DagRender.ts | 72 +++------------------------- src/common/panels.ts | 73 +++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 65 deletions(-) create mode 100644 src/common/panels.ts diff --git a/src/commands/pipelines/DagRender.ts b/src/commands/pipelines/DagRender.ts index d081edca..6e870f13 100644 --- a/src/commands/pipelines/DagRender.ts +++ b/src/commands/pipelines/DagRender.ts @@ -20,6 +20,7 @@ import { LSClient } from '../../services/LSClient'; import { ServerStatus } from '../../types/ServerInfoTypes'; import { JsonObject } from '../../views/panel/panelView/PanelTreeItem'; import { PanelDataProvider } from '../../views/panel/panelView/PanelDataProvider'; +import Panels from '../../common/panels'; const ROOT_PATH = ['resources', 'dag-view']; const CSS_FILE = 'dag.css'; @@ -28,7 +29,6 @@ const ICONS_DIRECTORY = '/resources/dag-view/icons/'; export default class DagRenderer { private static instance: DagRenderer | undefined; - private openPanels: { [id: string]: vscode.WebviewPanel }; private createSVGWindow: Function = () => {}; private iconSvgs: { [name: string]: string } = {}; private root: vscode.Uri; @@ -37,7 +37,6 @@ export default class DagRenderer { constructor(context: vscode.ExtensionContext) { DagRenderer.instance = this; - this.openPanels = {}; this.root = vscode.Uri.joinPath(context.extensionUri, ...ROOT_PATH); this.javaScript = vscode.Uri.joinPath(this.root, JS_FILE); this.css = vscode.Uri.joinPath(this.root, CSS_FILE); @@ -60,7 +59,6 @@ export default class DagRenderer { */ public deactivate(): void { DagRenderer.instance = undefined; - Object.values(this.openPanels).forEach(panel => panel.dispose()); } /** @@ -69,30 +67,21 @@ export default class DagRenderer { * @returns */ public async createView(node: PipelineTreeItem) { - const existingPanel = this.getDagPanel(node.id); + const p = Panels.getInstance(); + const existingPanel = p.getPanel(node.id); if (existingPanel) { existingPanel.reveal(); return; } - const panel = vscode.window.createWebviewPanel( - `DAG-${node.id}`, - node.label as string, - vscode.ViewColumn.One, - { - enableScripts: true, - localResourceRoots: [this.root], - } - ); - - panel.webview.html = this.getLoadingContent(); + const panel = p.createPanel(node.id, node.label as string, { + enableScripts: true, + localResourceRoots: [this.root], + }); panel.webview.onDidReceiveMessage(this.createMessageHandler(panel, node)); this.renderDag(panel, node); - - // To track which DAGs are currently open - this.registerDagPanel(node.id, panel); } private createMessageHandler( @@ -240,22 +229,6 @@ export default class DagRenderer { }); } - private deregisterDagPanel(runId: string) { - delete this.openPanels[runId]; - } - - private getDagPanel(runId: string): vscode.WebviewPanel | undefined { - return this.openPanels[runId]; - } - - private registerDagPanel(runId: string, panel: vscode.WebviewPanel) { - this.openPanels[runId] = panel; - - panel.onDidDispose(() => { - this.deregisterDagPanel(runId); - }, null); - } - private layoutDag(dagData: PipelineRunDag): Dagre.graphlib.Graph { const { nodes, edges } = dagData; const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); @@ -350,37 +323,6 @@ export default class DagRenderer { return canvas.svg(); } - private getLoadingContent(): string { - return ` - - - - - - - Loading - - - -
- -`; - } - private getWebviewContent({ svg, cssUri, diff --git a/src/common/panels.ts b/src/common/panels.ts new file mode 100644 index 00000000..e4ce30a9 --- /dev/null +++ b/src/common/panels.ts @@ -0,0 +1,73 @@ +import * as vscode from 'vscode'; + +export default class Panels { + private static instance: Panels | undefined; + private openPanels: { [id: string]: vscode.WebviewPanel }; + + constructor() { + this.openPanels = {}; + } + + public static getInstance(): Panels { + if (Panels.instance === undefined) { + Panels.instance = new Panels(); + } + return Panels.instance; + } + + public createPanel( + id: string, + label: string, + options?: vscode.WebviewPanelOptions & vscode.WebviewOptions + ) { + const panel = vscode.window.createWebviewPanel(id, label, vscode.ViewColumn.One, options); + panel.webview.html = this.getLoadingContent(); + + this.openPanels[id] = panel; + + panel.onDidDispose(() => { + this.deregisterPanel(id); + }, null); + + return panel; + } + + private deregisterPanel(id: string) { + delete this.openPanels[id]; + } + + public getPanel(id: string): vscode.WebviewPanel | undefined { + return this.openPanels[id]; + } + + private getLoadingContent(): string { + return ` + + + + + + + Loading + + + +
+ +`; + } +} From dfc34cc7459ce189fae80147de4c4b2c93dd6cd6 Mon Sep 17 00:00:00 2001 From: ErikWiens Date: Wed, 17 Jul 2024 13:58:32 -0400 Subject: [PATCH 19/45] feat: add panel opens when click on plus icon to create a new stack Co-authored-by: Christopher Perkins --- bundled/tool/lsp_zenml.py | 46 +++++++--- bundled/tool/zenml_wrappers.py | 149 +++++++++++++++++++++++---------- package.json | 15 +++- src/commands/stack/cmds.ts | 12 +++ src/commands/stack/registry.ts | 4 + 5 files changed, 167 insertions(+), 59 deletions(-) diff --git a/bundled/tool/lsp_zenml.py b/bundled/tool/lsp_zenml.py index f8f4e3fe..5a20164b 100644 --- a/bundled/tool/lsp_zenml.py +++ b/bundled/tool/lsp_zenml.py @@ -32,7 +32,9 @@ from zen_watcher import ZenConfigWatcher from zenml_client import ZenMLClient -zenml_init_error = {"error": "ZenML is not initialized. Please check ZenML version requirements."} +zenml_init_error = { + "error": "ZenML is not initialized. Please check ZenML version requirements." +} class ZenLanguageServer(LanguageServer): @@ -58,7 +60,9 @@ async def is_zenml_installed(self) -> bool: if process.returncode == 0: self.show_message_log("✅ ZenML installation check: Successful.") return True - self.show_message_log("❌ ZenML installation check failed.", lsp.MessageType.Error) + self.show_message_log( + "❌ ZenML installation check failed.", lsp.MessageType.Error + ) return False except Exception as e: self.show_message_log( @@ -93,7 +97,9 @@ async def initialize_zenml_client(self): # initialize watcher self.initialize_global_config_watcher() except Exception as e: - self.notify_user(f"Failed to initialize ZenML client: {str(e)}", lsp.MessageType.Error) + self.notify_user( + f"Failed to initialize ZenML client: {str(e)}", lsp.MessageType.Error + ) def initialize_global_config_watcher(self): """Sets up and starts the Global Configuration Watcher.""" @@ -133,7 +139,9 @@ def wrapper(*args, **kwargs): with suppress_stdout_temporarily(): if wrapper_name: - wrapper_instance = getattr(self.zenml_client, wrapper_name, None) + wrapper_instance = getattr( + self.zenml_client, wrapper_name, None + ) if not wrapper_instance: return {"error": f"Wrapper '{wrapper_name}' not found."} return func(wrapper_instance, *args, **kwargs) @@ -177,25 +185,33 @@ def _construct_version_validation_response(self, meets_requirement, version_str) def send_custom_notification(self, method: str, args: dict): """Sends a custom notification to the LSP client.""" - self.show_message_log(f"Sending custom notification: {method} with args: {args}") + self.show_message_log( + f"Sending custom notification: {method} with args: {args}" + ) self.send_notification(method, args) def update_python_interpreter(self, interpreter_path): """Updates the Python interpreter path and handles errors.""" try: self.python_interpreter = interpreter_path - self.show_message_log(f"LSP_Python_Interpreter Updated: {self.python_interpreter}") + self.show_message_log( + f"LSP_Python_Interpreter Updated: {self.python_interpreter}" + ) # pylint: disable=broad-exception-caught except Exception as e: self.show_message_log( f"Failed to update Python interpreter: {str(e)}", lsp.MessageType.Error ) - def notify_user(self, message: str, msg_type: lsp.MessageType = lsp.MessageType.Info): + def notify_user( + self, message: str, msg_type: lsp.MessageType = lsp.MessageType.Info + ): """Logs a message and also notifies the user.""" self.show_message(message, msg_type) - def log_to_output(self, message: str, msg_type: lsp.MessageType = lsp.MessageType.Log) -> None: + def log_to_output( + self, message: str, msg_type: lsp.MessageType = lsp.MessageType.Log + ) -> None: """Log to output.""" self.show_message_log(message, msg_type) @@ -273,13 +289,13 @@ def fetch_pipeline_runs(wrapper_instance, args): def delete_pipeline_run(wrapper_instance, args): """Deletes a specified ZenML pipeline run.""" return wrapper_instance.delete_pipeline_run(args) - + @self.command(f"{TOOL_MODULE_NAME}.getPipelineRun") @self.zenml_command(wrapper_name="pipeline_runs_wrapper") def get_pipeline_run(wrapper_instance, args): """Gets a specified ZenML pipeline run.""" return wrapper_instance.get_pipeline_run(args) - + @self.command(f"{TOOL_MODULE_NAME}.getPipelineRunStep") @self.zenml_command(wrapper_name="pipeline_runs_wrapper") def get_run_step(wrapper_instance, args): @@ -291,9 +307,15 @@ def get_run_step(wrapper_instance, args): def get_run_artifact(wrapper_instance, args): """Gets a specified ZenML pipeline artifact""" return wrapper_instance.get_run_artifact(args) - + @self.command(f"{TOOL_MODULE_NAME}.getPipelineRunDag") @self.zenml_command(wrapper_name="pipeline_runs_wrapper") def get_run_dag(wrapper_instance, args): """Gets graph data for a specified ZenML pipeline run""" - return wrapper_instance.get_pipeline_run_graph(args) \ No newline at end of file + return wrapper_instance.get_pipeline_run_graph(args) + + @self.command(f"{TOOL_MODULE_NAME}.listComponents") + @self.zenml_command(wrapper_name="stacks_wrapper") + def list_components(wrapper_instance, args): + """Get paginated stack components from ZenML""" + return wrapper_instance.list_components(args) diff --git a/bundled/tool/zenml_wrappers.py b/bundled/tool/zenml_wrappers.py index b4953ff2..acbe0b32 100644 --- a/bundled/tool/zenml_wrappers.py +++ b/bundled/tool/zenml_wrappers.py @@ -48,7 +48,9 @@ def get_global_config_directory(self): def RestZenStoreConfiguration(self): """Returns the RestZenStoreConfiguration class for store configuration.""" # pylint: disable=not-callable - return self.lazy_import("zenml.zen_stores.rest_zen_store", "RestZenStoreConfiguration") + return self.lazy_import( + "zenml.zen_stores.rest_zen_store", "RestZenStoreConfiguration" + ) def get_global_config_directory_path(self) -> str: """Get the global configuration directory path. @@ -189,7 +191,9 @@ def get_server_info(self) -> ZenmlServerInfoResp: # Handle both 'store' and 'store_configuration' depending on version store_attr_name = ( - "store_configuration" if hasattr(self.gc, "store_configuration") else "store" + "store_configuration" + if hasattr(self.gc, "store_configuration") + else "store" ) store_config = getattr(self.gc, store_attr_name) @@ -229,7 +233,9 @@ def connect(self, args, **kwargs) -> dict: try: # pylint: disable=not-callable access_token = self.web_login(url=url, verify_ssl=verify_ssl) - self._config_wrapper.set_store_configuration(remote_url=url, access_token=access_token) + self._config_wrapper.set_store_configuration( + remote_url=url, access_token=access_token + ) return {"message": "Connected successfully.", "access_token": access_token} except self.AuthorizationException as e: return {"error": f"Authorization failed: {str(e)}"} @@ -245,7 +251,9 @@ def disconnect(self, args) -> dict: try: # Adjust for changes from 'store' to 'store_configuration' store_attr_name = ( - "store_configuration" if hasattr(self.gc, "store_configuration") else "store" + "store_configuration" + if hasattr(self.gc, "store_configuration") + else "store" ) url = getattr(self.gc, store_attr_name).url store_type = self.BaseZenStore.get_store_type(url) @@ -314,15 +322,21 @@ def fetch_pipeline_runs(self, args): "version": run.body.pipeline.body.version, "stackName": run.body.stack.name, "startTime": ( - run.metadata.start_time.isoformat() if run.metadata.start_time else None + run.metadata.start_time.isoformat() + if run.metadata.start_time + else None ), "endTime": ( - run.metadata.end_time.isoformat() if run.metadata.end_time else None + run.metadata.end_time.isoformat() + if run.metadata.end_time + else None ), "os": run.metadata.client_environment.get("os", "Unknown OS"), "osVersion": run.metadata.client_environment.get( "os_version", - run.metadata.client_environment.get("mac_version", "Unknown Version"), + run.metadata.client_environment.get( + "mac_version", "Unknown Version" + ), ), "pythonVersion": run.metadata.client_environment.get( "python_version", "Unknown" @@ -357,10 +371,10 @@ def delete_pipeline_run(self, args) -> dict: return {"message": f"Pipeline run `{run_id}` deleted successfully."} except self.ZenMLBaseException as e: return {"error": f"Failed to delete pipeline run: {str(e)}"} - + def get_pipeline_run(self, args: Tuple[str]) -> dict: """Gets a ZenML pipeline run. - + Args: args (list): List of arguments. Returns: @@ -371,33 +385,39 @@ def get_pipeline_run(self, args: Tuple[str]) -> dict: run = self.client.get_pipeline_run(run_id, hydrate=True) run_data = { "id": str(run.id), - "name": run.body.pipeline.name, - "status": run.body.status, - "version": run.body.pipeline.body.version, - "stackName": run.body.stack.name, - "startTime": ( - run.metadata.start_time.isoformat() if run.metadata.start_time else None - ), - "endTime": ( - run.metadata.end_time.isoformat() if run.metadata.end_time else None - ), - "os": run.metadata.client_environment.get("os", "Unknown OS"), - "osVersion": run.metadata.client_environment.get( - "os_version", - run.metadata.client_environment.get("mac_version", "Unknown Version"), - ), - "pythonVersion": run.metadata.client_environment.get( - "python_version", "Unknown" + "name": run.body.pipeline.name, + "status": run.body.status, + "version": run.body.pipeline.body.version, + "stackName": run.body.stack.name, + "startTime": ( + run.metadata.start_time.isoformat() + if run.metadata.start_time + else None + ), + "endTime": ( + run.metadata.end_time.isoformat() if run.metadata.end_time else None + ), + "os": run.metadata.client_environment.get("os", "Unknown OS"), + "osVersion": run.metadata.client_environment.get( + "os_version", + run.metadata.client_environment.get( + "mac_version", "Unknown Version" ), + ), + "pythonVersion": run.metadata.client_environment.get( + "python_version", "Unknown" + ), } return run_data except self.ZenMLBaseException as e: return {"error": f"Failed to retrieve pipeline run: {str(e)}"} - - def get_pipeline_run_graph(self, args: Tuple[str]) -> Union[GraphResponse, ErrorResponse]: + + def get_pipeline_run_graph( + self, args: Tuple[str] + ) -> Union[GraphResponse, ErrorResponse]: """Gets a ZenML pipeline run step DAG. - + Args: args (list): List of arguments. Returns: @@ -415,7 +435,7 @@ def get_pipeline_run_graph(self, args: Tuple[str]) -> Union[GraphResponse, Error def get_run_step(self, args: Tuple[str]) -> Union[RunStepResponse, ErrorResponse]: """Gets a ZenML pipeline run step. - + Args: args (list): List of arguments. Returns: @@ -424,7 +444,9 @@ def get_run_step(self, args: Tuple[str]) -> Union[RunStepResponse, ErrorResponse try: step_run_id = args[0] step = self.client.get_run_step(step_run_id, hydrate=True) - run = self.client.get_pipeline_run(step.metadata.pipeline_run_id, hydrate=True) + run = self.client.get_pipeline_run( + step.metadata.pipeline_run_id, hydrate=True + ) step_data = { "name": step.name, @@ -435,18 +457,22 @@ def get_run_step(self, args: Tuple[str]) -> Union[RunStepResponse, ErrorResponse "email": step.body.user.name, }, "startTime": ( - step.metadata.start_time.isoformat() if step.metadata.start_time else None + step.metadata.start_time.isoformat() + if step.metadata.start_time + else None ), "endTime": ( - step.metadata.end_time.isoformat() if step.metadata.end_time else None + step.metadata.end_time.isoformat() + if step.metadata.end_time + else None ), "duration": ( - str(step.metadata.end_time - step.metadata.start_time) if step.metadata.end_time and step.metadata.start_time else None + str(step.metadata.end_time - step.metadata.start_time) + if step.metadata.end_time and step.metadata.start_time + else None ), "stackName": run.body.stack.name, - "orchestrator": { - "runId": str(run.metadata.orchestrator_run_id) - }, + "orchestrator": {"runId": str(run.metadata.orchestrator_run_id)}, "pipeline": { "name": run.body.pipeline.name, "status": run.body.status, @@ -454,15 +480,17 @@ def get_run_step(self, args: Tuple[str]) -> Union[RunStepResponse, ErrorResponse }, "cacheKey": step.metadata.cache_key, "sourceCode": step.metadata.source_code, - "logsUri": step.metadata.logs.body.uri + "logsUri": step.metadata.logs.body.uri, } return step_data except self.ZenMLBaseException as e: return {"error": f"Failed to retrieve pipeline run step: {str(e)}"} - - def get_run_artifact(self, args: Tuple[str]) -> Union[RunArtifactResponse, ErrorResponse]: + + def get_run_artifact( + self, args: Tuple[str] + ) -> Union[RunArtifactResponse, ErrorResponse]: """Gets a ZenML pipeline run artifact. - + Args: args (list): List of arguments. Returns: @@ -540,7 +568,9 @@ def fetch_stacks(self, args): return {"error": "Insufficient arguments provided."} page, max_size = args try: - stacks_page = self.client.list_stacks(page=page, size=max_size, hydrate=True) + stacks_page = self.client.list_stacks( + page=page, size=max_size, hydrate=True + ) stacks_data = self.process_stacks(stacks_page.items) return { @@ -653,17 +683,23 @@ def copy_stack(self, args) -> dict: target_stack_name = args[1] if not source_stack_name_or_id or not target_stack_name: - return {"error": "Both source stack name/id and target stack name are required"} + return { + "error": "Both source stack name/id and target stack name are required" + } try: - stack_to_copy = self.client.get_stack(name_id_or_prefix=source_stack_name_or_id) + stack_to_copy = self.client.get_stack( + name_id_or_prefix=source_stack_name_or_id + ) component_mapping = { c_type: [c.id for c in components][0] for c_type, components in stack_to_copy.components.items() if components } - self.client.create_stack(name=target_stack_name, components=component_mapping) + self.client.create_stack( + name=target_stack_name, components=component_mapping + ) return { "message": ( f"Stack `{source_stack_name_or_id}` successfully copied " @@ -675,3 +711,26 @@ def copy_stack(self, args) -> dict: self.StackComponentValidationError, ) as e: return {"error": str(e)} + + def list_components(self, args) -> dict: + page = args[0] + components = self.client.list_stack_components(page=page, hydrate=True) + return { + "index": components.index, + "max_size": components.max_size, + "total_pages": components.total_pages, + "total": components.total, + "items": [ + { + "id": str(item.id), + "name": item.name, + "flavor": item.body.flavor, + "type": item.body.type, + } + for item in components.items + ], + } + + # index, max_size, total_pages, total + # items [] + # id, name, body.flavor, body.type diff --git a/package.json b/package.json index 99bfd6b0..bffa2ec5 100644 --- a/package.json +++ b/package.json @@ -255,6 +255,12 @@ "title": "Restart LSP Server", "icon": "$(debug-restart)", "category": "ZenML Environment" + }, + { + "command": "zenml.createStack", + "title": "Create Stack", + "icon": "$(add)", + "category": "ZenML Stacks" } ], "viewsContainers": { @@ -322,14 +328,19 @@ }, { "when": "stackCommandsRegistered && view == zenmlStackView", - "command": "zenml.setStackItemsPerPage", + "command": "zenml.createStack", "group": "navigation@1" }, { "when": "stackCommandsRegistered && view == zenmlStackView", - "command": "zenml.refreshStackView", + "command": "zenml.setStackItemsPerPage", "group": "navigation@2" }, + { + "when": "stackCommandsRegistered && view == zenmlStackView", + "command": "zenml.refreshStackView", + "group": "navigation@3" + }, { "when": "pipelineCommandsRegistered && view == zenmlPipelineView", "command": "zenml.setPipelineRunsPerPage", diff --git a/src/commands/stack/cmds.ts b/src/commands/stack/cmds.ts index 9525824a..5c7b42e5 100644 --- a/src/commands/stack/cmds.ts +++ b/src/commands/stack/cmds.ts @@ -16,6 +16,8 @@ import ZenMLStatusBar from '../../views/statusBar'; import { getStackDashboardUrl, switchActiveStack } from './utils'; import { LSClient } from '../../services/LSClient'; import { showInformationMessage } from '../../utils/notifications'; +import Panels from '../../common/panels'; +import { randomUUID } from 'crypto'; /** * Refreshes the stack view. @@ -157,6 +159,15 @@ const setActiveStack = async (node: StackTreeItem): Promise => { ); }; +const createStack = async () => { + console.log('Creating a stack!'); + const id = 'stack form'; + const label = 'Create Stack'; + Panels.getInstance().createPanel(id, label); + const obj = await LSClient.getInstance().sendLsClientRequest('listComponents', [1]); + console.log(obj); +}; + /** * Opens the selected stack in the ZenML Dashboard in the browser * @@ -185,4 +196,5 @@ export const stackCommands = { copyStack, setActiveStack, goToStackUrl, + createStack, }; diff --git a/src/commands/stack/registry.ts b/src/commands/stack/registry.ts index e8973ccd..b6a4e38a 100644 --- a/src/commands/stack/registry.ts +++ b/src/commands/stack/registry.ts @@ -35,6 +35,10 @@ export const registerStackCommands = (context: ExtensionContext) => { 'zenml.refreshActiveStack', async () => await stackCommands.refreshActiveStack() ), + registerCommand( + 'zenml.createStack', + async () => await stackCommands.createStack() + ), registerCommand( 'zenml.renameStack', async (node: StackTreeItem) => await stackCommands.renameStack(node) From 8b67d448ad76f95eafedb65e83d7b4b516236af7 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Thu, 18 Jul 2024 15:11:53 -0400 Subject: [PATCH 20/45] feat: Added Stack Components view to activity bar --- bundled/tool/type_hints.py | 18 +- bundled/tool/zenml_wrappers.py | 58 +++-- package.json | 5 + src/commands/stack/cmds.ts | 2 +- src/services/ZenExtension.ts | 2 + src/types/StackTypes.ts | 15 +- .../activityBar/common/LoadingTreeItem.ts | 1 + .../componentView/ComponentDataProvider.ts | 230 ++++++++++++++++++ .../activityBar/stackView/StackTreeItems.ts | 8 +- 9 files changed, 312 insertions(+), 27 deletions(-) create mode 100644 src/views/activityBar/componentView/ComponentDataProvider.ts diff --git a/bundled/tool/type_hints.py b/bundled/tool/type_hints.py index f5441944..8f7b2357 100644 --- a/bundled/tool/type_hints.py +++ b/bundled/tool/type_hints.py @@ -1,7 +1,6 @@ from typing import Any, TypedDict, Dict, List, Union from uuid import UUID - class StepArtifactBody(TypedDict): type: str artifact: Dict[str, str] @@ -20,8 +19,6 @@ class GraphEdge(TypedDict): source: str target: str - - class GraphResponse(TypedDict): nodes: List[GraphNode] edges: List[GraphEdge] @@ -84,4 +81,17 @@ class ZenmlGlobalConfigResp(TypedDict): version: str active_stack_id: str active_workspace_name: str - store: ZenmlStoreConfig \ No newline at end of file + store: ZenmlStoreConfig + +class ComponentResponse(TypedDict): + id: str + name: str + flavor: str + type: str + +class ListComponentsResponse(TypedDict): + index: int + max_size: int + total_pages: int + total: int + items: List[ComponentResponse] \ No newline at end of file diff --git a/bundled/tool/zenml_wrappers.py b/bundled/tool/zenml_wrappers.py index acbe0b32..fe77394c 100644 --- a/bundled/tool/zenml_wrappers.py +++ b/bundled/tool/zenml_wrappers.py @@ -14,8 +14,16 @@ import pathlib from typing import Any, Tuple, Union -from type_hints import GraphResponse, ErrorResponse, RunStepResponse, RunArtifactResponse, ZenmlServerInfoResp, ZenmlGlobalConfigResp from zenml_grapher import Grapher +from type_hints import ( + GraphResponse, + ErrorResponse, + RunStepResponse, + RunArtifactResponse, + ZenmlServerInfoResp, + ZenmlGlobalConfigResp, + ListComponentsResponse, +) class GlobalConfigWrapper: @@ -712,24 +720,38 @@ def copy_stack(self, args) -> dict: ) as e: return {"error": str(e)} - def list_components(self, args) -> dict: + def list_components(self, args: Tuple[int, int, Union[str, None]]) -> Union[ListComponentsResponse,ErrorResponse]: + if len(args) < 2: + return {"error": "Insufficient arguments provided."} + page = args[0] - components = self.client.list_stack_components(page=page, hydrate=True) - return { - "index": components.index, - "max_size": components.max_size, - "total_pages": components.total_pages, - "total": components.total, - "items": [ - { - "id": str(item.id), - "name": item.name, - "flavor": item.body.flavor, - "type": item.body.type, - } - for item in components.items - ], - } + max_size = args[1] + filter = None + + if len(args) >= 3: + filter = args[2] + + try: + components = self.client.list_stack_components(page=page, size=max_size, type=filter, hydrate=True) + + return { + "index": components.index, + "max_size": components.max_size, + "total_pages": components.total_pages, + "total": components.total, + "items": [ + { + "id": str(item.id), + "name": item.name, + "flavor": item.body.flavor, + "type": item.body.type, + } + for item in components.items + ], + } + except self.ZenMLBaseException as e: + return {"error": f"Failed to retrieve list of stack components: {str(e)}"} + # index, max_size, total_pages, total # items [] diff --git a/package.json b/package.json index bffa2ec5..4a9d1f91 100644 --- a/package.json +++ b/package.json @@ -291,6 +291,11 @@ "name": "Stacks", "icon": "$(layers)" }, + { + "id": "zenmlComponentView", + "name": "Stack Components", + "icon": "$(extensions)" + }, { "id": "zenmlPipelineView", "name": "Pipeline Runs", diff --git a/src/commands/stack/cmds.ts b/src/commands/stack/cmds.ts index 5c7b42e5..9f2a4b4d 100644 --- a/src/commands/stack/cmds.ts +++ b/src/commands/stack/cmds.ts @@ -164,7 +164,7 @@ const createStack = async () => { const id = 'stack form'; const label = 'Create Stack'; Panels.getInstance().createPanel(id, label); - const obj = await LSClient.getInstance().sendLsClientRequest('listComponents', [1]); + const obj = await LSClient.getInstance().sendLsClientRequest('listComponents', [1, 10]); console.log(obj); }; diff --git a/src/services/ZenExtension.ts b/src/services/ZenExtension.ts index 3836cabf..3905598b 100644 --- a/src/services/ZenExtension.ts +++ b/src/services/ZenExtension.ts @@ -41,6 +41,7 @@ import ZenMLStatusBar from '../views/statusBar'; import { LSClient } from './LSClient'; import { toggleCommands } from '../utils/global'; import { PanelDataProvider } from '../views/panel/panelView/PanelDataProvider'; +import { ComponentDataProvider } from '../views/activityBar/componentView/ComponentDataProvider'; export interface IServerInfo { name: string; @@ -61,6 +62,7 @@ export class ZenExtension { private static dataProviders = new Map>([ ['zenmlServerView', ServerDataProvider.getInstance()], ['zenmlStackView', StackDataProvider.getInstance()], + ['zenmlComponentView', ComponentDataProvider.getInstance()], ['zenmlPipelineView', PipelineDataProvider.getInstance()], ['zenmlPanelView', PanelDataProvider.getInstance()], ]); diff --git a/src/types/StackTypes.ts b/src/types/StackTypes.ts index 5ada4087..d8666320 100644 --- a/src/types/StackTypes.ts +++ b/src/types/StackTypes.ts @@ -44,4 +44,17 @@ interface StackComponent { export type StacksResponse = StacksData | ErrorMessageResponse | VersionMismatchError; -export { Stack, Components, StackComponent, StacksData }; +interface ComponentsListData { + index: number; + max_size: number; + total_pages: number; + total: number; + items: Array; +} + +export type ComponentsListResponse = + | ComponentsListData + | ErrorMessageResponse + | VersionMismatchError; + +export { Stack, Components, StackComponent, StacksData, ComponentsListData }; diff --git a/src/views/activityBar/common/LoadingTreeItem.ts b/src/views/activityBar/common/LoadingTreeItem.ts index 9e0fe9b6..df52fdb5 100644 --- a/src/views/activityBar/common/LoadingTreeItem.ts +++ b/src/views/activityBar/common/LoadingTreeItem.ts @@ -23,6 +23,7 @@ export class LoadingTreeItem extends TreeItem { export const LOADING_TREE_ITEMS = new Map([ ['server', new LoadingTreeItem('Refreshing Server View...')], ['stacks', new LoadingTreeItem('Refreshing Stacks View...')], + ['components', new LoadingTreeItem('Refreshing Components View...')], ['pipelineRuns', new LoadingTreeItem('Refreshing Pipeline Runs...')], ['environment', new LoadingTreeItem('Refreshing Environments...')], ['lsClient', new LoadingTreeItem('Waiting for Language Server to start...', '')], diff --git a/src/views/activityBar/componentView/ComponentDataProvider.ts b/src/views/activityBar/componentView/ComponentDataProvider.ts new file mode 100644 index 00000000..537c6f22 --- /dev/null +++ b/src/views/activityBar/componentView/ComponentDataProvider.ts @@ -0,0 +1,230 @@ +// 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 { Event, EventEmitter, TreeDataProvider, TreeItem, window, workspace } from 'vscode'; +import { State } from 'vscode-languageclient'; +import { EventBus } from '../../../services/EventBus'; +import { LSClient } from '../../../services/LSClient'; +import { + ITEMS_PER_PAGE_OPTIONS, + LSCLIENT_STATE_CHANGED, + LSP_ZENML_CLIENT_INITIALIZED, + LSP_ZENML_STACK_CHANGED, +} from '../../../utils/constants'; +import { ErrorTreeItem, createErrorItem, createAuthErrorItem } from '../common/ErrorTreeItem'; +import { LOADING_TREE_ITEMS } from '../common/LoadingTreeItem'; +import { CommandTreeItem } from '../common/PaginationTreeItems'; +import { ComponentsListResponse, StackComponent } from '../../../types/StackTypes'; +import { StackComponentTreeItem } from '../stackView/StackTreeItems'; + +export class ComponentDataProvider implements TreeDataProvider { + private _onDidChangeTreeData = new EventEmitter(); + readonly onDidChangeTreeData: Event = + this._onDidChangeTreeData.event; + + private static instance: ComponentDataProvider | null = null; + private eventBus = EventBus.getInstance(); + private zenmlClientReady = false; + public components: TreeItem[] = [LOADING_TREE_ITEMS.get('components')!]; + + private pagination = { + currentPage: 1, + itemsPerPage: 10, + totalItems: 0, + totalPages: 0, + }; + + constructor() { + this.subscribeToEvents(); + } + + /** + * Subscribes to relevant events to trigger a refresh of the tree view. + */ + public subscribeToEvents(): void { + this.eventBus.on(LSCLIENT_STATE_CHANGED, (newState: State) => { + if (newState === State.Running) { + this.refresh(); + } else { + this.components = [LOADING_TREE_ITEMS.get('lsClient')!]; + this._onDidChangeTreeData.fire(undefined); + } + }); + + this.eventBus.on(LSP_ZENML_CLIENT_INITIALIZED, (isInitialized: boolean) => { + this.zenmlClientReady = isInitialized; + + if (!isInitialized) { + this.components = [LOADING_TREE_ITEMS.get('stacks')!]; + this._onDidChangeTreeData.fire(undefined); + return; + } + this.refresh(); + this.eventBus.off(LSP_ZENML_STACK_CHANGED, () => this.refresh()); + this.eventBus.on(LSP_ZENML_STACK_CHANGED, () => this.refresh()); + }); + } + + /** + * Retrieves the singleton instance of ComponentDataProvider + * + * @returns {ComponentDataProvider} The signleton instance. + */ + public static getInstance(): ComponentDataProvider { + if (!ComponentDataProvider.instance) { + ComponentDataProvider.instance = new ComponentDataProvider(); + } + + return ComponentDataProvider.instance; + } + + /** + * Returns the provided tree item. + * + * @param element element The tree item to return. + * @returns The corresponding VS Code tree item + */ + public getTreeItem(element: TreeItem): TreeItem { + return element; + } + + public async refresh(): Promise { + this.components = [LOADING_TREE_ITEMS.get('components')!]; + this._onDidChangeTreeData.fire(undefined); + + const page = this.pagination.currentPage; + const itemsPerPage = this.pagination.itemsPerPage; + + try { + const newComponentsData = await this.fetchComponents(page, itemsPerPage); + this.components = newComponentsData; + } catch (e) { + this.components = createErrorItem(e); + } + + this._onDidChangeTreeData.fire(undefined); + } + + public async goToNextPage() { + if (this.pagination.currentPage < this.pagination.totalPages) { + this.pagination.currentPage++; + await this.refresh(); + } + } + + public async goToPreviousPage() { + if (this.pagination.currentPage > 1) { + this.pagination.currentPage--; + await this.refresh(); + } + } + + public async updateItemsPerPage() { + const selected = await window.showQuickPick(ITEMS_PER_PAGE_OPTIONS, { + placeHolder: 'Choose the max number of stacks to display per page', + }); + if (selected) { + this.pagination.itemsPerPage = parseInt(selected, 10); + this.pagination.currentPage = 1; + await this.refresh(); + } + } + + public async getChildren(element?: TreeItem): Promise { + if (!element) { + if (Array.isArray(this.components) && this.components.length > 0) { + return this.components; + } + + const components = await this.fetchComponents( + this.pagination.currentPage, + this.pagination.itemsPerPage + ); + return components; + } + + return undefined; + } + + private async fetchComponents(page: number = 1, itemsPerPage: number = 10) { + if (!this.zenmlClientReady) { + return [LOADING_TREE_ITEMS.get('zenmlClient')!]; + } + + try { + const lsClient = LSClient.getInstance(); + const result = await lsClient.sendLsClientRequest('listComponents', [ + page, + itemsPerPage, + ]); + + if (Array.isArray(result) && result.length === 1 && 'error' in result[0]) { + const errorMessage = result[0].error; + if (errorMessage.includes('Authentication error')) { + return createAuthErrorItem(errorMessage); + } + } + + if (!result || 'error' in result) { + if ('clientVersion' in result && 'serverVersion' in result) { + return createErrorItem(result); + } else { + console.error(`Failed to fetch stack components: ${result.error}`); + return []; + } + } + + if ('items' in result) { + const { items, total, total_pages, index, max_size } = result; + this.pagination = { + currentPage: index, + itemsPerPage: max_size, + totalItems: total, + totalPages: total_pages, + }; + + const components = items.map( + (component: StackComponent) => new StackComponentTreeItem(component) + ); + return this.addPaginationCommands(components); + } else { + console.error('Unexpected response format:', result); + return []; + } + } catch (e: any) { + console.error(`Failed to fetch components: ${e}`); + return [ + new ErrorTreeItem('Error', `Failed to fetch components: ${e.message || e.toString()}`), + ]; + } + } + + private addPaginationCommands(treeItems: TreeItem[]): TreeItem[] { + if (this.pagination.currentPage < this.pagination.totalPages) { + treeItems.push( + new CommandTreeItem('Next Page', 'zenml.nextComponentPage', undefined, 'arrow-circle-right') + ); + } + + if (this.pagination.currentPage > 1) { + treeItems.unshift( + new CommandTreeItem( + 'Previous Page', + 'zenml.previousComponentPage', + undefined, + 'arrow-circle-left' + ) + ); + } + return treeItems; + } +} diff --git a/src/views/activityBar/stackView/StackTreeItems.ts b/src/views/activityBar/stackView/StackTreeItems.ts index 524e8993..ea54c802 100644 --- a/src/views/activityBar/stackView/StackTreeItems.ts +++ b/src/views/activityBar/stackView/StackTreeItems.ts @@ -44,13 +44,15 @@ export class StackTreeItem extends vscode.TreeItem { export class StackComponentTreeItem extends vscode.TreeItem { constructor( public component: StackComponent, - public stackId: string + public stackId?: string ) { super(component.name, vscode.TreeItemCollapsibleState.None); - this.tooltip = `Type: ${component.type}, Flavor: ${component.flavor}, ID: ${stackId}`; + this.tooltip = stackId + ? `Type: ${component.type}, Flavor: ${component.flavor}, ID: ${stackId}` + : `Type: ${component.type}, Flavor: ${component.flavor}`; this.description = `${component.type} (${component.flavor})`; this.contextValue = 'stackComponent'; - this.id = `${stackId}-${component.id}`; + this.id = stackId ? `${stackId}-${component.id}` : `${component.id}`; } } From 85dd63c70917638536fd8189e1d38e8dc7e65eb0 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Thu, 18 Jul 2024 15:43:20 -0400 Subject: [PATCH 21/45] feat: Added Refresh and ItemsPerPage commands to Components View --- package.json | 22 +++++++++ src/commands/components/cmds.ts | 36 ++++++++++++++ src/commands/components/registry.ts | 48 +++++++++++++++++++ src/services/ZenExtension.ts | 2 + src/utils/global.ts | 1 + .../componentView/ComponentDataProvider.ts | 2 +- 6 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/commands/components/cmds.ts create mode 100644 src/commands/components/registry.ts diff --git a/package.json b/package.json index 4a9d1f91..ecacab1b 100644 --- a/package.json +++ b/package.json @@ -196,6 +196,18 @@ "icon": "$(check)", "category": "ZenML Stacks" }, + { + "command": "zenml.setComponentItemsPerPage", + "title": "Set Components Per Page", + "icon": "$(layers)", + "category": "ZenML Components" + }, + { + "command": "zenml.refreshComponentView", + "title": "Refresh Component View", + "icon": "$(refresh)", + "category": "ZenML Components" + }, { "command": "zenml.copyStack", "title": "Copy Stack", @@ -346,6 +358,16 @@ "command": "zenml.refreshStackView", "group": "navigation@3" }, + { + "when": "componentCommandsRegistered && view == zenmlComponentView", + "command": "zenml.setComponentItemsPerPage", + "group": "navigation@1" + }, + { + "when": "componentCommandsRegistered && view == zenmlComponentView", + "command": "zenml.refreshComponentView", + "group": "navigation@2" + }, { "when": "pipelineCommandsRegistered && view == zenmlPipelineView", "command": "zenml.setPipelineRunsPerPage", diff --git a/src/commands/components/cmds.ts b/src/commands/components/cmds.ts new file mode 100644 index 00000000..8e33eb3d --- /dev/null +++ b/src/commands/components/cmds.ts @@ -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 * as vscode from 'vscode'; + +import ZenMLStatusBar from '../../views/statusBar'; +import { LSClient } from '../../services/LSClient'; +import { showInformationMessage } from '../../utils/notifications'; +import Panels from '../../common/panels'; +import { ComponentDataProvider } from '../../views/activityBar/componentView/ComponentDataProvider'; + +const refreshComponentView = async () => { + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Refreshing Component View...', + cancellable: false, + }, + async progress => { + await ComponentDataProvider.getInstance().refresh(); + } + ); +}; + +export const componentCommands = { + refreshComponentView, +}; diff --git a/src/commands/components/registry.ts b/src/commands/components/registry.ts new file mode 100644 index 00000000..eac8e31a --- /dev/null +++ b/src/commands/components/registry.ts @@ -0,0 +1,48 @@ +// 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 { componentCommands } from './cmds'; +import { registerCommand } from '../../common/vscodeapi'; +import { ZenExtension } from '../../services/ZenExtension'; +import { ExtensionContext, commands } from 'vscode'; +import { ComponentDataProvider } from '../../views/activityBar/componentView/ComponentDataProvider'; + +/** + * Registers stack component-related commands for the extension. + * + * @param {ExtensionContext} context - The context in which the extension operates, used for registering commands and managing their lifecycle. + */ +export const registerComponentCommands = (context: ExtensionContext) => { + const componentDataProvider = ComponentDataProvider.getInstance(); + try { + const registeredCommands = [ + registerCommand( + 'zenml.setComponentItemsPerPage', + async () => await componentDataProvider.updateItemsPerPage() + ), + registerCommand( + 'zenml.refreshComponentView', + async () => await componentCommands.refreshComponentView() + ), + ]; + + registeredCommands.forEach(cmd => { + context.subscriptions.push(cmd); + ZenExtension.commandDisposables.push(cmd); + }); + + commands.executeCommand('setContext', 'componentCommandsRegistered', true); + } catch (e) { + console.error('Error registering component commands:', e); + commands.executeCommand('setContext', 'componentCommandsRegistered', false); + } +}; diff --git a/src/services/ZenExtension.ts b/src/services/ZenExtension.ts index 3905598b..43a0f8e7 100644 --- a/src/services/ZenExtension.ts +++ b/src/services/ZenExtension.ts @@ -42,6 +42,7 @@ import { LSClient } from './LSClient'; import { toggleCommands } from '../utils/global'; import { PanelDataProvider } from '../views/panel/panelView/PanelDataProvider'; import { ComponentDataProvider } from '../views/activityBar/componentView/ComponentDataProvider'; +import { registerComponentCommands } from '../commands/components/registry'; export interface IServerInfo { name: string; @@ -70,6 +71,7 @@ export class ZenExtension { private static registries = [ registerServerCommands, registerStackCommands, + registerComponentCommands, registerPipelineCommands, ]; diff --git a/src/utils/global.ts b/src/utils/global.ts index 8645a153..f14700f5 100644 --- a/src/utils/global.ts +++ b/src/utils/global.ts @@ -122,6 +122,7 @@ export function getDefaultPythonInterpreterPath(): string { * @param state The state to set the commands to. */ export async function toggleCommands(state: boolean): Promise { + await vscode.commands.executeCommand('setContext', 'stackCommandsRegistered', state); await vscode.commands.executeCommand('setContext', 'stackCommandsRegistered', state); await vscode.commands.executeCommand('setContext', 'serverCommandsRegistered', state); await vscode.commands.executeCommand('setContext', 'pipelineCommandsRegistered', state); diff --git a/src/views/activityBar/componentView/ComponentDataProvider.ts b/src/views/activityBar/componentView/ComponentDataProvider.ts index 537c6f22..7b109092 100644 --- a/src/views/activityBar/componentView/ComponentDataProvider.ts +++ b/src/views/activityBar/componentView/ComponentDataProvider.ts @@ -64,7 +64,7 @@ export class ComponentDataProvider implements TreeDataProvider { this.zenmlClientReady = isInitialized; if (!isInitialized) { - this.components = [LOADING_TREE_ITEMS.get('stacks')!]; + this.components = [LOADING_TREE_ITEMS.get('components')!]; this._onDidChangeTreeData.fire(undefined); return; } From 47af9e7edf02ea19f6344b09ed1481df7822b0fe Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Thu, 18 Jul 2024 16:24:19 -0400 Subject: [PATCH 22/45] chore: Refactored Pagination in DataProviders to base class --- src/commands/components/registry.ts | 8 ++ src/utils/global.ts | 2 +- .../common/PaginatedDataProvider.ts | 111 ++++++++++++++++++ .../componentView/ComponentDataProvider.ts | 103 ++-------------- .../pipelineView/PipelineDataProvider.ts | 111 ++---------------- .../stackView/StackDataProvider.ts | 101 ++-------------- src/views/statusBar/index.ts | 10 +- 7 files changed, 159 insertions(+), 287 deletions(-) create mode 100644 src/views/activityBar/common/PaginatedDataProvider.ts diff --git a/src/commands/components/registry.ts b/src/commands/components/registry.ts index eac8e31a..392e04fa 100644 --- a/src/commands/components/registry.ts +++ b/src/commands/components/registry.ts @@ -33,6 +33,14 @@ export const registerComponentCommands = (context: ExtensionContext) => { 'zenml.refreshComponentView', async () => await componentCommands.refreshComponentView() ), + registerCommand( + 'zenml.nextComponentPage', + async () => await componentDataProvider.goToNextPage() + ), + registerCommand( + 'zenml.previousComponentPage', + async () => await componentDataProvider.goToPreviousPage() + ), ]; registeredCommands.forEach(cmd => { diff --git a/src/utils/global.ts b/src/utils/global.ts index f14700f5..a4f94a3b 100644 --- a/src/utils/global.ts +++ b/src/utils/global.ts @@ -123,7 +123,7 @@ export function getDefaultPythonInterpreterPath(): string { */ export async function toggleCommands(state: boolean): Promise { await vscode.commands.executeCommand('setContext', 'stackCommandsRegistered', state); - await vscode.commands.executeCommand('setContext', 'stackCommandsRegistered', state); + await vscode.commands.executeCommand('setContext', 'componentCommandsRegistered', state); await vscode.commands.executeCommand('setContext', 'serverCommandsRegistered', state); await vscode.commands.executeCommand('setContext', 'pipelineCommandsRegistered', state); await vscode.commands.executeCommand('setContext', 'environmentCommandsRegistered', state); diff --git a/src/views/activityBar/common/PaginatedDataProvider.ts b/src/views/activityBar/common/PaginatedDataProvider.ts new file mode 100644 index 00000000..7d61a36f --- /dev/null +++ b/src/views/activityBar/common/PaginatedDataProvider.ts @@ -0,0 +1,111 @@ +// 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 { Event, EventEmitter, TreeDataProvider, TreeItem, window } from 'vscode'; +import { ITEMS_PER_PAGE_OPTIONS } from '../../../utils/constants'; +import { CommandTreeItem } from './PaginationTreeItems'; +import { LoadingTreeItem } from './LoadingTreeItem'; +import { ErrorTreeItem } from './ErrorTreeItem'; + +export class PaginatedDataProvider implements TreeDataProvider { + protected _onDidChangeTreeData = new EventEmitter(); + readonly onDidChangeTreeData: Event = + this._onDidChangeTreeData.event; + protected pagination = { + currentPage: 1, + itemsPerPage: 10, + totalItems: 0, + totalPages: 0, + }; + public items: TreeItem[] = []; + protected viewName: string = ''; + + public async goToNextPage() { + if (this.pagination.currentPage < this.pagination.totalPages) { + this.pagination.currentPage++; + await this.refresh(); + } + } + + public async goToPreviousPage() { + if (this.pagination.currentPage > 1) { + this.pagination.currentPage--; + await this.refresh(); + } + } + + public async updateItemsPerPage() { + const selected = await window.showQuickPick(ITEMS_PER_PAGE_OPTIONS, { + placeHolder: 'Choose the max number of items to display per page', + }); + if (selected) { + this.pagination.itemsPerPage = parseInt(selected, 10); + this.pagination.currentPage = 1; + await this.refresh(); + } + } + + public async refresh(): Promise { + this._onDidChangeTreeData.fire(undefined); + } + + /** + * Returns the provided tree item. + * + * @param element element The tree item to return. + * @returns The corresponding VS Code tree item + */ + public getTreeItem(element: TreeItem): TreeItem { + return element; + } + + public async getChildren(element?: TreeItem): Promise { + if (!element) { + if (this.items[0] instanceof LoadingTreeItem || this.items[0] instanceof ErrorTreeItem) { + return this.items; + } + return this.addPaginationCommands(this.items.slice()); + } + + if ('children' in element && Array.isArray(element.children)) { + return element.children; + } + + return undefined; + } + + private addPaginationCommands(treeItems: TreeItem[]): TreeItem[] { + if (this.pagination.currentPage < this.pagination.totalPages) { + treeItems.push( + new CommandTreeItem( + 'Next Page', + `zenml.next${this.viewName}Page`, + undefined, + 'arrow-circle-right' + ) + ); + } + + if (this.pagination.currentPage > 1) { + treeItems.unshift( + new CommandTreeItem( + 'Previous Page', + `zenml.previous${this.viewName}Page`, + undefined, + 'arrow-circle-left' + ) + ); + } + return treeItems; + } +} diff --git a/src/views/activityBar/componentView/ComponentDataProvider.ts b/src/views/activityBar/componentView/ComponentDataProvider.ts index 7b109092..9c302857 100644 --- a/src/views/activityBar/componentView/ComponentDataProvider.ts +++ b/src/views/activityBar/componentView/ComponentDataProvider.ts @@ -10,12 +10,10 @@ // 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 { Event, EventEmitter, TreeDataProvider, TreeItem, window, workspace } from 'vscode'; import { State } from 'vscode-languageclient'; import { EventBus } from '../../../services/EventBus'; import { LSClient } from '../../../services/LSClient'; import { - ITEMS_PER_PAGE_OPTIONS, LSCLIENT_STATE_CHANGED, LSP_ZENML_CLIENT_INITIALIZED, LSP_ZENML_STACK_CHANGED, @@ -25,26 +23,18 @@ import { LOADING_TREE_ITEMS } from '../common/LoadingTreeItem'; import { CommandTreeItem } from '../common/PaginationTreeItems'; import { ComponentsListResponse, StackComponent } from '../../../types/StackTypes'; import { StackComponentTreeItem } from '../stackView/StackTreeItems'; +import { PaginatedDataProvider } from '../common/PaginatedDataProvider'; -export class ComponentDataProvider implements TreeDataProvider { - private _onDidChangeTreeData = new EventEmitter(); - readonly onDidChangeTreeData: Event = - this._onDidChangeTreeData.event; - +export class ComponentDataProvider extends PaginatedDataProvider { private static instance: ComponentDataProvider | null = null; private eventBus = EventBus.getInstance(); private zenmlClientReady = false; - public components: TreeItem[] = [LOADING_TREE_ITEMS.get('components')!]; - - private pagination = { - currentPage: 1, - itemsPerPage: 10, - totalItems: 0, - totalPages: 0, - }; constructor() { + super(); this.subscribeToEvents(); + this.items = [LOADING_TREE_ITEMS.get('components')!]; + this.viewName = 'Component'; } /** @@ -55,7 +45,7 @@ export class ComponentDataProvider implements TreeDataProvider { if (newState === State.Running) { this.refresh(); } else { - this.components = [LOADING_TREE_ITEMS.get('lsClient')!]; + this.items = [LOADING_TREE_ITEMS.get('lsClient')!]; this._onDidChangeTreeData.fire(undefined); } }); @@ -64,7 +54,7 @@ export class ComponentDataProvider implements TreeDataProvider { this.zenmlClientReady = isInitialized; if (!isInitialized) { - this.components = [LOADING_TREE_ITEMS.get('components')!]; + this.items = [LOADING_TREE_ITEMS.get('components')!]; this._onDidChangeTreeData.fire(undefined); return; } @@ -87,18 +77,8 @@ export class ComponentDataProvider implements TreeDataProvider { return ComponentDataProvider.instance; } - /** - * Returns the provided tree item. - * - * @param element element The tree item to return. - * @returns The corresponding VS Code tree item - */ - public getTreeItem(element: TreeItem): TreeItem { - return element; - } - public async refresh(): Promise { - this.components = [LOADING_TREE_ITEMS.get('components')!]; + this.items = [LOADING_TREE_ITEMS.get('components')!]; this._onDidChangeTreeData.fire(undefined); const page = this.pagination.currentPage; @@ -106,55 +86,14 @@ export class ComponentDataProvider implements TreeDataProvider { try { const newComponentsData = await this.fetchComponents(page, itemsPerPage); - this.components = newComponentsData; + this.items = newComponentsData; } catch (e) { - this.components = createErrorItem(e); + this.items = createErrorItem(e); } this._onDidChangeTreeData.fire(undefined); } - public async goToNextPage() { - if (this.pagination.currentPage < this.pagination.totalPages) { - this.pagination.currentPage++; - await this.refresh(); - } - } - - public async goToPreviousPage() { - if (this.pagination.currentPage > 1) { - this.pagination.currentPage--; - await this.refresh(); - } - } - - public async updateItemsPerPage() { - const selected = await window.showQuickPick(ITEMS_PER_PAGE_OPTIONS, { - placeHolder: 'Choose the max number of stacks to display per page', - }); - if (selected) { - this.pagination.itemsPerPage = parseInt(selected, 10); - this.pagination.currentPage = 1; - await this.refresh(); - } - } - - public async getChildren(element?: TreeItem): Promise { - if (!element) { - if (Array.isArray(this.components) && this.components.length > 0) { - return this.components; - } - - const components = await this.fetchComponents( - this.pagination.currentPage, - this.pagination.itemsPerPage - ); - return components; - } - - return undefined; - } - private async fetchComponents(page: number = 1, itemsPerPage: number = 10) { if (!this.zenmlClientReady) { return [LOADING_TREE_ITEMS.get('zenmlClient')!]; @@ -195,7 +134,7 @@ export class ComponentDataProvider implements TreeDataProvider { const components = items.map( (component: StackComponent) => new StackComponentTreeItem(component) ); - return this.addPaginationCommands(components); + return components; } else { console.error('Unexpected response format:', result); return []; @@ -207,24 +146,4 @@ export class ComponentDataProvider implements TreeDataProvider { ]; } } - - private addPaginationCommands(treeItems: TreeItem[]): TreeItem[] { - if (this.pagination.currentPage < this.pagination.totalPages) { - treeItems.push( - new CommandTreeItem('Next Page', 'zenml.nextComponentPage', undefined, 'arrow-circle-right') - ); - } - - if (this.pagination.currentPage > 1) { - treeItems.unshift( - new CommandTreeItem( - 'Previous Page', - 'zenml.previousComponentPage', - undefined, - 'arrow-circle-left' - ) - ); - } - return treeItems; - } } diff --git a/src/views/activityBar/pipelineView/PipelineDataProvider.ts b/src/views/activityBar/pipelineView/PipelineDataProvider.ts index f707cc91..fc83d062 100644 --- a/src/views/activityBar/pipelineView/PipelineDataProvider.ts +++ b/src/views/activityBar/pipelineView/PipelineDataProvider.ts @@ -25,27 +25,20 @@ import { ErrorTreeItem, createErrorItem, createAuthErrorItem } from '../common/E import { LOADING_TREE_ITEMS } from '../common/LoadingTreeItem'; import { PipelineRunTreeItem, PipelineTreeItem } from './PipelineTreeItems'; import { CommandTreeItem } from '../common/PaginationTreeItems'; +import { PaginatedDataProvider } from '../common/PaginatedDataProvider'; /** * Provides data for the pipeline run tree view, displaying detailed information about each pipeline run. */ -export class PipelineDataProvider implements TreeDataProvider { - private _onDidChangeTreeData = new EventEmitter(); - readonly onDidChangeTreeData = this._onDidChangeTreeData.event; - +export class PipelineDataProvider extends PaginatedDataProvider { private static instance: PipelineDataProvider | null = null; private eventBus = EventBus.getInstance(); private zenmlClientReady = false; - private pipelineRuns: PipelineTreeItem[] | TreeItem[] = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; - - private pagination = { - currentPage: 1, - itemsPerPage: 20, - totalItems: 0, - totalPages: 0, - }; constructor() { + super(); + this.items = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; + this.viewName = 'PipelineRuns'; this.subscribeToEvents(); } @@ -57,7 +50,7 @@ export class PipelineDataProvider implements TreeDataProvider { if (newState === State.Running) { this.refresh(); } else { - this.pipelineRuns = [LOADING_TREE_ITEMS.get('lsClient')!]; + this.items = [LOADING_TREE_ITEMS.get('lsClient')!]; this._onDidChangeTreeData.fire(undefined); } }); @@ -66,7 +59,7 @@ export class PipelineDataProvider implements TreeDataProvider { this.zenmlClientReady = isInitialized; if (!isInitialized) { - this.pipelineRuns = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; + this.items = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; this._onDidChangeTreeData.fire(undefined); return; } @@ -83,10 +76,10 @@ export class PipelineDataProvider implements TreeDataProvider { * @returns {PipelineDataProvider} The singleton instance. */ public static getInstance(): PipelineDataProvider { - if (!this.instance) { - this.instance = new PipelineDataProvider(); + if (!PipelineDataProvider.instance) { + PipelineDataProvider.instance = new PipelineDataProvider(); } - return this.instance; + return PipelineDataProvider.instance; } /** @@ -95,74 +88,21 @@ export class PipelineDataProvider implements TreeDataProvider { * @returns A promise resolving to void. */ public async refresh(): Promise { - this.pipelineRuns = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; + this.items = [LOADING_TREE_ITEMS.get('pipelineRuns')!]; this._onDidChangeTreeData.fire(undefined); const page = this.pagination.currentPage; const itemsPerPage = this.pagination.itemsPerPage; try { const newPipelineData = await this.fetchPipelineRuns(page, itemsPerPage); - this.pipelineRuns = newPipelineData; + this.items = newPipelineData; } catch (error: any) { - this.pipelineRuns = createErrorItem(error); + this.items = createErrorItem(error); } this._onDidChangeTreeData.fire(undefined); } - /** - * Retrieves the tree item for a given pipeline run. - * - * @param element The pipeline run item. - * @returns The corresponding VS Code tree item. - */ - getTreeItem(element: TreeItem): TreeItem { - return element; - } - - /** - * Retrieves the children for a given tree item. - * - * @param element The parent tree item. If undefined, root pipeline runs are fetched. - * @returns A promise resolving to an array of child tree items or undefined if there are no children. - */ - async getChildren(element?: TreeItem): Promise { - if (!element) { - if (Array.isArray(this.pipelineRuns) && this.pipelineRuns.length > 0) { - return this.pipelineRuns; - } - - // Fetch pipeline runs for the current page and add pagination controls if necessary - const runs = await this.fetchPipelineRuns( - this.pagination.currentPage, - this.pagination.itemsPerPage - ); - if (this.pagination.currentPage < this.pagination.totalPages) { - runs.push( - new CommandTreeItem( - 'Next Page', - 'zenml.nextPipelineRunsPage', - undefined, - 'arrow-circle-right' - ) - ); - } - if (this.pagination.currentPage > 1) { - runs.unshift( - new CommandTreeItem( - 'Previous Page', - 'zenml.previousPipelineRunsPage', - undefined, - 'arrow-circle-left' - ) - ); - } - return runs; - } else if (element instanceof PipelineTreeItem) { - return element.children; - } - return undefined; - } /** * Fetches pipeline runs from the server and maps them to tree items for display. * @@ -234,29 +174,4 @@ export class PipelineDataProvider implements TreeDataProvider { ]; } } - - public async goToNextPage() { - if (this.pagination.currentPage < this.pagination.totalPages) { - this.pagination.currentPage++; - await this.refresh(); - } - } - - public async goToPreviousPage() { - if (this.pagination.currentPage > 1) { - this.pagination.currentPage--; - await this.refresh(); - } - } - - public async updateItemsPerPage() { - const selected = await window.showQuickPick(ITEMS_PER_PAGE_OPTIONS, { - placeHolder: 'Choose the max number of pipeline runs to display per page', - }); - if (selected) { - this.pagination.itemsPerPage = parseInt(selected, 10); - this.pagination.currentPage = 1; - await this.refresh(); - } - } } diff --git a/src/views/activityBar/stackView/StackDataProvider.ts b/src/views/activityBar/stackView/StackDataProvider.ts index 9dfe1b76..b611767d 100644 --- a/src/views/activityBar/stackView/StackDataProvider.ts +++ b/src/views/activityBar/stackView/StackDataProvider.ts @@ -25,26 +25,18 @@ import { ErrorTreeItem, createErrorItem, createAuthErrorItem } from '../common/E import { LOADING_TREE_ITEMS } from '../common/LoadingTreeItem'; import { StackComponentTreeItem, StackTreeItem } from './StackTreeItems'; import { CommandTreeItem } from '../common/PaginationTreeItems'; +import { PaginatedDataProvider } from '../common/PaginatedDataProvider'; -export class StackDataProvider implements TreeDataProvider { - private _onDidChangeTreeData = new EventEmitter(); - readonly onDidChangeTreeData: Event = - this._onDidChangeTreeData.event; - +export class StackDataProvider extends PaginatedDataProvider { private static instance: StackDataProvider | null = null; private eventBus = EventBus.getInstance(); private zenmlClientReady = false; - public stacks: StackTreeItem[] | TreeItem[] = [LOADING_TREE_ITEMS.get('stacks')!]; - - private pagination = { - currentPage: 1, - itemsPerPage: 20, - totalItems: 0, - totalPages: 0, - }; constructor() { + super(); this.subscribeToEvents(); + this.items = [LOADING_TREE_ITEMS.get('stacks')!]; + this.viewName = 'Stack'; } /** @@ -55,7 +47,7 @@ export class StackDataProvider implements TreeDataProvider { if (newState === State.Running) { this.refresh(); } else { - this.stacks = [LOADING_TREE_ITEMS.get('lsClient')!]; + this.items = [LOADING_TREE_ITEMS.get('lsClient')!]; this._onDidChangeTreeData.fire(undefined); } }); @@ -64,7 +56,7 @@ export class StackDataProvider implements TreeDataProvider { this.zenmlClientReady = isInitialized; if (!isInitialized) { - this.stacks = [LOADING_TREE_ITEMS.get('stacks')!]; + this.items = [LOADING_TREE_ITEMS.get('stacks')!]; this._onDidChangeTreeData.fire(undefined); return; } @@ -86,23 +78,13 @@ export class StackDataProvider implements TreeDataProvider { return this.instance; } - /** - * Returns the provided tree item. - * - * @param {TreeItem} element The tree item to return. - * @returns The corresponding VS Code tree item. - */ - getTreeItem(element: TreeItem): TreeItem { - return element; - } - /** * Refreshes the tree view data by refetching stacks and triggering the onDidChangeTreeData event. * * @returns {Promise} A promise that resolves when the tree view data has been refreshed. */ public async refresh(): Promise { - this.stacks = [LOADING_TREE_ITEMS.get('stacks')!]; + this.items = [LOADING_TREE_ITEMS.get('stacks')!]; this._onDidChangeTreeData.fire(undefined); const page = this.pagination.currentPage; @@ -110,9 +92,9 @@ export class StackDataProvider implements TreeDataProvider { try { const newStacksData = await this.fetchStacksWithComponents(page, itemsPerPage); - this.stacks = newStacksData; + this.items = newStacksData; } catch (error: any) { - this.stacks = createErrorItem(error); + this.items = createErrorItem(error); } this._onDidChangeTreeData.fire(undefined); @@ -179,69 +161,6 @@ export class StackDataProvider implements TreeDataProvider { } } - public async goToNextPage() { - if (this.pagination.currentPage < this.pagination.totalPages) { - this.pagination.currentPage++; - await this.refresh(); - } - } - - public async goToPreviousPage() { - if (this.pagination.currentPage > 1) { - this.pagination.currentPage--; - await this.refresh(); - } - } - - public async updateItemsPerPage() { - const selected = await window.showQuickPick(ITEMS_PER_PAGE_OPTIONS, { - placeHolder: 'Choose the max number of stacks to display per page', - }); - if (selected) { - this.pagination.itemsPerPage = parseInt(selected, 10); - this.pagination.currentPage = 1; - await this.refresh(); - } - } - - /** - * Retrieves the children of a given tree item. - * - * @param {TreeItem} element The tree item whose children to retrieve. - * @returns A promise resolving to an array of child tree items or undefined if there are no children. - */ - async getChildren(element?: TreeItem): Promise { - if (!element) { - if (Array.isArray(this.stacks) && this.stacks.length > 0) { - return this.stacks; - } - - const stacks = await this.fetchStacksWithComponents( - this.pagination.currentPage, - this.pagination.itemsPerPage - ); - if (this.pagination.currentPage < this.pagination.totalPages) { - stacks.push( - new CommandTreeItem('Next Page', 'zenml.nextStackPage', undefined, 'arrow-circle-right') - ); - } - if (this.pagination.currentPage > 1) { - stacks.unshift( - new CommandTreeItem( - 'Previous Page', - 'zenml.previousStackPage', - undefined, - 'arrow-circle-left' - ) - ); - } - return stacks; - } else if (element instanceof StackTreeItem) { - return element.children; - } - return undefined; - } - /** * Helper method to determine if a stack is the active stack. * diff --git a/src/views/statusBar/index.ts b/src/views/statusBar/index.ts index c6db6759..d931a52e 100644 --- a/src/views/statusBar/index.ts +++ b/src/views/statusBar/index.ts @@ -116,17 +116,17 @@ export default class ZenMLStatusBar { */ private async switchStack(): Promise { const stackDataProvider = StackDataProvider.getInstance(); - const { stacks } = stackDataProvider; + const { items } = stackDataProvider; - const containsErrors = stacks.some(stack => stack instanceof ErrorTreeItem); + const containsErrors = items.some(stack => stack instanceof ErrorTreeItem); - if (containsErrors || stacks.length === 0) { + if (containsErrors || items.length === 0) { window.showErrorMessage('No stacks available.'); return; } - const activeStack = stacks.find(stack => stack.id === this.activeStackId); - const otherStacks = stacks.filter(stack => stack.id !== this.activeStackId); + const activeStack = items.find(stack => stack.id === this.activeStackId); + const otherStacks = items.filter(stack => stack.id !== this.activeStackId); const quickPickItems = [ { From b62ee9cc30cf58e0ae390adce6d8e606db41b988 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Thu, 18 Jul 2024 19:45:33 -0400 Subject: [PATCH 23/45] feature: Added listFlavors command to LS --- bundled/tool/lsp_zenml.py | 24 +++++++--- bundled/tool/type_hints.py | 30 ++++++++++--- bundled/tool/zenml_wrappers.py | 44 +++++++++++++++++-- src/commands/stack/cmds.ts | 11 +++-- .../common/PaginatedDataProvider.ts | 6 ++- 5 files changed, 94 insertions(+), 21 deletions(-) diff --git a/bundled/tool/lsp_zenml.py b/bundled/tool/lsp_zenml.py index 5a20164b..bdfc4049 100644 --- a/bundled/tool/lsp_zenml.py +++ b/bundled/tool/lsp_zenml.py @@ -277,6 +277,24 @@ def rename_stack(wrapper_instance, args): def copy_stack(wrapper_instance, args): """Copies a specified ZenML stack to a new stack.""" return wrapper_instance.copy_stack(args) + + @self.command(f"{TOOL_MODULE_NAME}.listComponents") + @self.zenml_command(wrapper_name="stacks_wrapper") + def list_components(wrapper_instance, args): + """Get paginated stack components from ZenML""" + return wrapper_instance.list_components(args) + + @self.command(f"{TOOL_MODULE_NAME}.getComponentTypes") + @self.zenml_command(wrapper_name="stacks_wrapper") + def get_component_types(wrapper_instance, args): + """Get paginated stack components from ZenML""" + return wrapper_instance.get_component_types() + + @self.command(f"{TOOL_MODULE_NAME}.listFlavors") + @self.zenml_command(wrapper_name="stacks_wrapper") + def list_flavors(wrapper_instance, args): + """Get paginated stack components from ZenML""" + return wrapper_instance.list_flavors(args) @self.command(f"{TOOL_MODULE_NAME}.getPipelineRuns") @self.zenml_command(wrapper_name="pipeline_runs_wrapper") @@ -313,9 +331,3 @@ def get_run_artifact(wrapper_instance, args): def get_run_dag(wrapper_instance, args): """Gets graph data for a specified ZenML pipeline run""" return wrapper_instance.get_pipeline_run_graph(args) - - @self.command(f"{TOOL_MODULE_NAME}.listComponents") - @self.zenml_command(wrapper_name="stacks_wrapper") - def list_components(wrapper_instance, args): - """Get paginated stack components from ZenML""" - return wrapper_instance.list_components(args) diff --git a/bundled/tool/type_hints.py b/bundled/tool/type_hints.py index 8f7b2357..7d6247a6 100644 --- a/bundled/tool/type_hints.py +++ b/bundled/tool/type_hints.py @@ -1,6 +1,7 @@ -from typing import Any, TypedDict, Dict, List, Union +from typing import Any, TypedDict, Dict, List, Optional from uuid import UUID + class StepArtifactBody(TypedDict): type: str artifact: Dict[str, str] @@ -34,9 +35,9 @@ class RunStepResponse(TypedDict): id: str status: str author: Dict[str, str] - startTime: Union[str, None] - endTime: Union[str, None] - duration: Union[str, None] + startTime: Optional[str] + endTime: Optional[str] + duration: Optional[str] stackName: str orchestrator: Dict[str, str] pipeline: Dict[str, str] @@ -68,7 +69,7 @@ class ZenmlStoreInfo(TypedDict): class ZenmlStoreConfig(TypedDict): type: str url: str - api_token: Union[str, None] + api_token: Optional[str] class ZenmlServerInfoResp(TypedDict): store_info: ZenmlStoreInfo @@ -83,7 +84,7 @@ class ZenmlGlobalConfigResp(TypedDict): active_workspace_name: str store: ZenmlStoreConfig -class ComponentResponse(TypedDict): +class StackComponent(TypedDict): id: str name: str flavor: str @@ -94,4 +95,19 @@ class ListComponentsResponse(TypedDict): max_size: int total_pages: int total: int - items: List[ComponentResponse] \ No newline at end of file + items: List[StackComponent] + +class Flavor(TypedDict): + id: str + name: str + type: str + logo_url: str + description: str + config_schema: Dict[str, Any] + +class ListFlavorsResponse(TypedDict): + index: int + max_size: int + total_pages: int + total: int + items: List[StackComponent] \ No newline at end of file diff --git a/bundled/tool/zenml_wrappers.py b/bundled/tool/zenml_wrappers.py index fe77394c..11c52b18 100644 --- a/bundled/tool/zenml_wrappers.py +++ b/bundled/tool/zenml_wrappers.py @@ -13,7 +13,7 @@ """This module provides wrappers for ZenML configuration and operations.""" import pathlib -from typing import Any, Tuple, Union +from typing import Any, Tuple, Union, List, Optional from zenml_grapher import Grapher from type_hints import ( GraphResponse, @@ -23,6 +23,7 @@ ZenmlServerInfoResp, ZenmlGlobalConfigResp, ListComponentsResponse, + ListFlavorsResponse ) @@ -564,6 +565,11 @@ def IllegalOperationError(self) -> Any: def StackComponentValidationError(self): """Returns the ZenML StackComponentValidationError class.""" return self.lazy_import("zenml.exceptions", "StackComponentValidationError") + + @property + def StackComponentType(self): + """Returns the ZenML StackComponentType enum.""" + return self.lazy_import("zenml.enums", "StackComponentType") @property def ZenKeyError(self) -> Any: @@ -752,7 +758,37 @@ def list_components(self, args: Tuple[int, int, Union[str, None]]) -> Union[List except self.ZenMLBaseException as e: return {"error": f"Failed to retrieve list of stack components: {str(e)}"} + def get_component_types(self) -> List[str]: + return self.StackComponentType.values() + + def list_flavors(self, args: Tuple[int, int, Optional[str]]) -> Union[ListFlavorsResponse, ErrorResponse]: + if len(args) < 2: + return {"error": "Insufficient arguments provided."} + + page = args[0] + max_size = args[1] + filter = None + if len(args) >= 3: + filter = args[2] - # index, max_size, total_pages, total - # items [] - # id, name, body.flavor, body.type + try: + flavors = self.client.list_flavors(page=page, size=max_size, type=filter, hydrate=True) + + return { + "index": flavors.index, + "max_size": flavors.max_size, + "total_pages": flavors.total_pages, + "total": flavors.total, + "items": [ + { + "id": str(flavor.id), + "name": flavor.name, + "type": flavor.body.type, + "logo_url": flavor.body.logo_url, + "config_schema": flavor.metadata.config_schema, + } for flavor in flavors.items + ] + } + + except self.ZenMLBaseException as e: + return {"error": f"Failed to retrieve list of flavors: {str(e)}"} \ No newline at end of file diff --git a/src/commands/stack/cmds.ts b/src/commands/stack/cmds.ts index 9f2a4b4d..b71b3acd 100644 --- a/src/commands/stack/cmds.ts +++ b/src/commands/stack/cmds.ts @@ -161,10 +161,15 @@ const setActiveStack = async (node: StackTreeItem): Promise => { const createStack = async () => { console.log('Creating a stack!'); - const id = 'stack form'; + const id = 'stack-form'; const label = 'Create Stack'; - Panels.getInstance().createPanel(id, label); - const obj = await LSClient.getInstance().sendLsClientRequest('listComponents', [1, 10]); + // Panels.getInstance().createPanel(id, label); + // const obj = await LSClient.getInstance().sendLsClientRequest('listComponents', [1, 10]); + const obj = await LSClient.getInstance().sendLsClientRequest('listFlavors', [ + 1, + 10000, + 'orchestrator', + ]); console.log(obj); }; diff --git a/src/views/activityBar/common/PaginatedDataProvider.ts b/src/views/activityBar/common/PaginatedDataProvider.ts index 7d61a36f..50baea8c 100644 --- a/src/views/activityBar/common/PaginatedDataProvider.ts +++ b/src/views/activityBar/common/PaginatedDataProvider.ts @@ -71,7 +71,11 @@ export class PaginatedDataProvider implements TreeDataProvider { public async getChildren(element?: TreeItem): Promise { if (!element) { - if (this.items[0] instanceof LoadingTreeItem || this.items[0] instanceof ErrorTreeItem) { + if ( + this.items.length === 0 || + this.items[0] instanceof LoadingTreeItem || + this.items[0] instanceof ErrorTreeItem + ) { return this.items; } return this.addPaginationCommands(this.items.slice()); From 7366ff0b4d6c9d6d68a242d70b195a9ec1acd333 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Thu, 18 Jul 2024 20:29:18 -0400 Subject: [PATCH 24/45] chore: Moved extension context functionality to new base class for DagRenderer, so I can use it across other inheritted classes --- src/commands/pipelines/DagRender.ts | 22 ++++++++++++++++------ src/common/WebviewBase.ts | 21 +++++++++++++++++++++ src/extension.ts | 3 ++- 3 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 src/common/WebviewBase.ts diff --git a/src/commands/pipelines/DagRender.ts b/src/commands/pipelines/DagRender.ts index 6e870f13..93038818 100644 --- a/src/commands/pipelines/DagRender.ts +++ b/src/commands/pipelines/DagRender.ts @@ -21,13 +21,14 @@ import { ServerStatus } from '../../types/ServerInfoTypes'; import { JsonObject } from '../../views/panel/panelView/PanelTreeItem'; import { PanelDataProvider } from '../../views/panel/panelView/PanelDataProvider'; import Panels from '../../common/panels'; +import WebviewBase from '../../common/WebviewBase'; const ROOT_PATH = ['resources', 'dag-view']; const CSS_FILE = 'dag.css'; const JS_FILE = 'dag-packed.js'; const ICONS_DIRECTORY = '/resources/dag-view/icons/'; -export default class DagRenderer { +export default class DagRenderer extends WebviewBase { private static instance: DagRenderer | undefined; private createSVGWindow: Function = () => {}; private iconSvgs: { [name: string]: string } = {}; @@ -35,14 +36,19 @@ export default class DagRenderer { private javaScript: vscode.Uri; private css: vscode.Uri; - constructor(context: vscode.ExtensionContext) { - DagRenderer.instance = this; - this.root = vscode.Uri.joinPath(context.extensionUri, ...ROOT_PATH); + constructor() { + super(); + + if (WebviewBase.context === null) { + throw new Error('Extension Context Not Propagated'); + } + + this.root = vscode.Uri.joinPath(WebviewBase.context.extensionUri, ...ROOT_PATH); this.javaScript = vscode.Uri.joinPath(this.root, JS_FILE); this.css = vscode.Uri.joinPath(this.root, CSS_FILE); this.loadSvgWindowLib(); - this.loadIcons(context.extensionPath + ICONS_DIRECTORY); + this.loadIcons(WebviewBase.context.extensionPath + ICONS_DIRECTORY); } /** @@ -50,7 +56,11 @@ export default class DagRenderer { * * @returns {DagRenderer | undefined} The singleton instance if it exists */ - public static getInstance(): DagRenderer | undefined { + public static getInstance(): DagRenderer { + if (!DagRenderer.instance) { + DagRenderer.instance = new DagRenderer(); + } + return DagRenderer.instance; } diff --git a/src/common/WebviewBase.ts b/src/common/WebviewBase.ts new file mode 100644 index 00000000..acc43a80 --- /dev/null +++ b/src/common/WebviewBase.ts @@ -0,0 +1,21 @@ +// 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 * as vscode from 'vscode'; + +export default class WebviewBase { + protected static context: vscode.ExtensionContext | null = null; + + public static setContext(context: vscode.ExtensionContext) { + WebviewBase.context = context; + } +} diff --git a/src/extension.ts b/src/extension.ts index 45abea59..8bc37721 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -20,6 +20,7 @@ import { registerEnvironmentCommands } from './commands/environment/registry'; import { LSP_ZENML_CLIENT_INITIALIZED } from './utils/constants'; import { toggleCommands } from './utils/global'; import DagRenderer from './commands/pipelines/DagRender'; +import WebviewBase from './common/WebviewBase'; export async function activate(context: vscode.ExtensionContext) { const eventBus = EventBus.getInstance(); @@ -48,7 +49,7 @@ export async function activate(context: vscode.ExtensionContext) { }) ); - new DagRenderer(context); + WebviewBase.setContext(context); } /** From c01a62f06879495664ca699e88d8e1e4f208f1aa Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Thu, 18 Jul 2024 22:57:03 -0400 Subject: [PATCH 25/45] feat: Stack Form is now being rendered --- package-lock.json | 95 +++++++++++++++++++- package.json | 2 + resources/stacks-form/stacks.css | 79 +++++++++++++++++ resources/stacks-form/stacks.js | 0 src/commands/stack/StackForm.ts | 146 +++++++++++++++++++++++++++++++ src/commands/stack/cmds.ts | 15 +--- src/common/api.ts | 68 ++++++++++++++ src/types/StackTypes.ts | 21 ++++- 8 files changed, 409 insertions(+), 17 deletions(-) create mode 100644 resources/stacks-form/stacks.css create mode 100644 resources/stacks-form/stacks.js create mode 100644 src/commands/stack/StackForm.ts create mode 100644 src/common/api.ts diff --git a/package-lock.json b/package-lock.json index 8960e019..df46f7e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "axios": "^1.6.7", "dagre": "^0.8.5", "fs-extra": "^11.2.0", + "hbs": "^4.2.0", "svg-pan-zoom": "github:bumbu/svg-pan-zoom", "svgdom": "^0.1.19", "vscode-languageclient": "^9.0.1" @@ -22,6 +23,7 @@ "devDependencies": { "@types/dagre": "^0.7.52", "@types/fs-extra": "^11.0.4", + "@types/hbs": "^4.0.4", "@types/mocha": "^10.0.6", "@types/node": "^18.19.18", "@types/sinon": "^17.0.3", @@ -514,6 +516,16 @@ "@types/node": "*" } }, + "node_modules/@types/hbs": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/hbs/-/hbs-4.0.4.tgz", + "integrity": "sha512-GH3SIb2tzDBnTByUSOIVcD6AcLufnydBllTuFAIAGMhqPNbz8GL4tLryVdNqhq0NQEb5mVpu2FJOrUeqwJrPtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "handlebars": "^4.1.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2579,6 +2591,12 @@ "unicode-trie": "^2.0.0" } }, + "node_modules/foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==", + "license": "Apache2" + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -2798,6 +2816,36 @@ "lodash": "^4.17.15" } }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -2855,6 +2903,20 @@ "node": ">= 0.4" } }, + "node_modules/hbs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hbs/-/hbs-4.2.0.tgz", + "integrity": "sha512-dQwHnrfWlTk5PvG9+a45GYpg0VpX47ryKF8dULVd6DtwOE6TEcYQXQ5QM6nyOx/h7v3bvEQbdn19EDAcfUAgZg==", + "license": "MIT", + "dependencies": { + "handlebars": "4.7.7", + "walk": "2.3.15" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -3645,8 +3707,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "optional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3929,8 +3989,7 @@ "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "node_modules/nise": { "version": "5.1.9", @@ -5481,6 +5540,19 @@ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "dev": true }, + "node_modules/uglify-js": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.0.tgz", + "integrity": "sha512-wNKHUY2hYYkf6oSFfhwwiHo4WCHzHmzcXsqXYTN9ja3iApYIFbb2U6ics9hBcYLHcYGQoAlwnZlTrf3oF+BL/Q==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/underscore": { "version": "1.13.6", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", @@ -5627,6 +5699,15 @@ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, + "node_modules/walk": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.15.tgz", + "integrity": "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "foreachasync": "^3.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", @@ -5807,6 +5888,12 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, "node_modules/workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", diff --git a/package.json b/package.json index ecacab1b..9193efa1 100644 --- a/package.json +++ b/package.json @@ -431,6 +431,7 @@ "devDependencies": { "@types/dagre": "^0.7.52", "@types/fs-extra": "^11.0.4", + "@types/hbs": "^4.0.4", "@types/mocha": "^10.0.6", "@types/node": "^18.19.18", "@types/sinon": "^17.0.3", @@ -458,6 +459,7 @@ "axios": "^1.6.7", "dagre": "^0.8.5", "fs-extra": "^11.2.0", + "hbs": "^4.2.0", "svg-pan-zoom": "github:bumbu/svg-pan-zoom", "svgdom": "^0.1.19", "vscode-languageclient": "^9.0.1" diff --git a/resources/stacks-form/stacks.css b/resources/stacks-form/stacks.css new file mode 100644 index 00000000..870fd0ac --- /dev/null +++ b/resources/stacks-form/stacks.css @@ -0,0 +1,79 @@ +h2 { + text-align: center; +} + +input { + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); +} + +input[type='radio'] { + appearance: none; + width: 15px; + height: 15px; + border-radius: 50%; + background-clip: content-box; + border: 2px solid var(--vscode-editor-foreground); + background-color: var(--vscode-editor-background); +} + +input[type='radio']:checked { + background-color: var(--vscode-editor-foreground); + padding: 2px; +} + +p { + margin: 0; + padding: 0; +} + +.options { + display: flex; + flex-wrap: nowrap; + overflow-x: auto; + align-items: center; + width: 100%; + border: 2px var(--vscode-editor-foreground) solid; + border-radius: 5px; + scrollbar-color: var(--vscode-editor-foreground) var(--vscode-editor-background); +} + +.single-option { + display: flex; + flex-direction: row; + align-items: start; + justify-content: center; + padding: 5px; + margin: 10px; + flex-shrink: 0; +} + +.single-option input { + margin-right: 5px; +} + +.single-option label { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: #eee; + color: #111; + text-align: center; + padding: 5px; + border: 2px var(--vscode-editor-foreground) solid; + border-radius: 5px; +} + +.single-option img { + width: 50px; + height: 50px; + flex-shrink: 0; +} + +.center { + display: flex; + justify-content: center; + align-items: center; + margin: 10px; +} diff --git a/resources/stacks-form/stacks.js b/resources/stacks-form/stacks.js new file mode 100644 index 00000000..e69de29b diff --git a/src/commands/stack/StackForm.ts b/src/commands/stack/StackForm.ts new file mode 100644 index 00000000..4b188ef6 --- /dev/null +++ b/src/commands/stack/StackForm.ts @@ -0,0 +1,146 @@ +// 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 * as vscode from 'vscode'; +import WebviewBase from '../../common/WebviewBase'; +import { handlebars } from 'hbs'; +import Panels from '../../common/panels'; +import { getAllFlavors, getAllStackComponents } from '../../common/api'; +import { Flavor, StackComponent } from '../../types/StackTypes'; + +type MixedComponent = { name: string; id: string; url: string }; + +const ROOT_PATH = ['resources', 'stacks-form']; +const CSS_FILE = 'stacks.css'; +const JS_FILE = 'stacks.js'; + +export default class StackForm extends WebviewBase { + private static instance: StackForm | null = null; + + private root: vscode.Uri; + private javaScript: vscode.Uri; + private css: vscode.Uri; + private template: HandlebarsTemplateDelegate; + + public static getInstance(): StackForm { + if (!StackForm.instance) { + StackForm.instance = new StackForm(); + } + + return StackForm.instance; + } + + constructor() { + super(); + + if (WebviewBase.context === null) { + throw new Error('Extension Context Not Propagated'); + } + + this.root = vscode.Uri.joinPath(WebviewBase.context.extensionUri, ...ROOT_PATH); + this.javaScript = vscode.Uri.joinPath(this.root, JS_FILE); + this.css = vscode.Uri.joinPath(this.root, CSS_FILE); + + handlebars.registerHelper('capitalize', (str: string) => { + return str + .split('_') + .map(word => word[0].toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); + }); + + this.template = handlebars.compile(this.produceTemplate()); + } + + public async display() { + const panels = Panels.getInstance(); + const existingPanel = panels.getPanel('stack-form'); + if (existingPanel) { + existingPanel.reveal(); + return; + } + + const panel = panels.createPanel('stack-form', 'Stack Form', { + enableForms: true, + enableScripts: true, + retainContextWhenHidden: true, + }); + + await this.renderForm(panel); + } + + private async renderForm(panel: vscode.WebviewPanel) { + const flavors = await getAllFlavors(); + const components = await getAllStackComponents(); + const options = this.convertComponents(flavors, components); + const js = panel.webview.asWebviewUri(this.javaScript); + const css = panel.webview.asWebviewUri(this.css); + + panel.webview.html = this.template({ options, js, css }); + } + + private convertComponents( + flavors: Flavor[], + components: { [type: string]: StackComponent[] } + ): { [type: string]: MixedComponent[] } { + const out: { [type: string]: MixedComponent[] } = {}; + + Object.keys(components).forEach(key => { + out[key] = components[key].map(component => { + return { + name: component.name, + id: component.id, + url: + flavors.find( + flavor => flavor.type === component.type && flavor.name === component.flavor + )?.logo_url ?? '', + }; + }); + }); + + return out; + } + + private produceTemplate(): string { + return ` + + + + + + + + Stack Form + + +

Create Stack

+
+ + {{#each options}} +

{{capitalize @key}}

+
+ {{#each this}} +
+ + +
+ {{/each}} +
+ {{/each}} +
+
+ + + + `; + } +} diff --git a/src/commands/stack/cmds.ts b/src/commands/stack/cmds.ts index b71b3acd..f8f53779 100644 --- a/src/commands/stack/cmds.ts +++ b/src/commands/stack/cmds.ts @@ -18,6 +18,7 @@ import { LSClient } from '../../services/LSClient'; import { showInformationMessage } from '../../utils/notifications'; import Panels from '../../common/panels'; import { randomUUID } from 'crypto'; +import StackForm from './StackForm'; /** * Refreshes the stack view. @@ -159,18 +160,8 @@ const setActiveStack = async (node: StackTreeItem): Promise => { ); }; -const createStack = async () => { - console.log('Creating a stack!'); - const id = 'stack-form'; - const label = 'Create Stack'; - // Panels.getInstance().createPanel(id, label); - // const obj = await LSClient.getInstance().sendLsClientRequest('listComponents', [1, 10]); - const obj = await LSClient.getInstance().sendLsClientRequest('listFlavors', [ - 1, - 10000, - 'orchestrator', - ]); - console.log(obj); +const createStack = () => { + StackForm.getInstance().display(); }; /** diff --git a/src/common/api.ts b/src/common/api.ts new file mode 100644 index 00000000..64b7d492 --- /dev/null +++ b/src/common/api.ts @@ -0,0 +1,68 @@ +import { LSClient } from '../services/LSClient'; +import { + ComponentsListResponse, + Flavor, + FlavorListResponse, + StackComponent, +} from '../types/StackTypes'; + +let flavors: Flavor[] = []; + +export const getAllFlavors = async (): Promise => { + if (flavors.length > 0) { + return flavors; + } + const lsClient = LSClient.getInstance(); + + let [page, maxPage] = [0, 1]; + do { + page++; + const resp = await lsClient.sendLsClientRequest('listFlavors', [ + page, + 10000, + ]); + + if ('error' in resp) { + console.error(`Error retrieving flavors: ${resp.error.toString()}`); + throw new Error(resp.error); + } + + maxPage = resp.total_pages; + flavors = flavors.concat(resp.items); + } while (page < maxPage); + return flavors; +}; + +export const getAllStackComponents = async (): Promise<{ + [type: string]: StackComponent[]; +}> => { + const lsClient = LSClient.getInstance(); + let components: StackComponent[] = []; + let [page, maxPage] = [0, 1]; + + do { + page++; + const resp = await lsClient.sendLsClientRequest('listComponents', [ + page, + 10000, + ]); + + if ('error' in resp) { + console.error(`Error retrieving components: ${resp.error.toString()}`); + throw new Error(resp.error); + } + + maxPage = resp.total_pages; + components = components.concat(resp.items); + } while (page < maxPage); + + const out: { [type: string]: StackComponent[] } = {}; + components.forEach(component => { + if (!(component.type in out)) { + out[component.type] = []; + } + out[component.type].push(component); + }); + + return out; +}; diff --git a/src/types/StackTypes.ts b/src/types/StackTypes.ts index d8666320..930fec6a 100644 --- a/src/types/StackTypes.ts +++ b/src/types/StackTypes.ts @@ -57,4 +57,23 @@ export type ComponentsListResponse = | ErrorMessageResponse | VersionMismatchError; -export { Stack, Components, StackComponent, StacksData, ComponentsListData }; +interface Flavor { + id: string; + name: string; + type: string; + logo_url: string; + description: string; + config_schema: { [key: string]: any }; +} + +interface FlavorListData { + index: number; + max_size: number; + total_pages: number; + total: number; + items: Flavor[]; +} + +export type FlavorListResponse = FlavorListData | ErrorMessageResponse | VersionMismatchError; + +export { Stack, Components, StackComponent, StacksData, ComponentsListData, Flavor }; From 90b223c378760eccefd40c30da78fd691379dce4 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Fri, 19 Jul 2024 16:12:03 -0400 Subject: [PATCH 26/45] feature: Added ability to Create/Update Stacks --- bundled/tool/lsp_zenml.py | 12 ++++ bundled/tool/zenml_wrappers.py | 25 ++++++- package.json | 6 +- resources/stacks-form/stacks.css | 24 +++++++ resources/stacks-form/stacks.js | 87 ++++++++++++++++++++++ src/commands/stack/StackForm.ts | 120 ++++++++++++++++++++++++++++++- src/commands/stack/cmds.ts | 25 +++++-- src/commands/stack/registry.ts | 6 +- 8 files changed, 290 insertions(+), 15 deletions(-) diff --git a/bundled/tool/lsp_zenml.py b/bundled/tool/lsp_zenml.py index bdfc4049..310276c8 100644 --- a/bundled/tool/lsp_zenml.py +++ b/bundled/tool/lsp_zenml.py @@ -278,6 +278,18 @@ def copy_stack(wrapper_instance, args): """Copies a specified ZenML stack to a new stack.""" return wrapper_instance.copy_stack(args) + @self.command(f"{TOOL_MODULE_NAME}.createStack") + @self.zenml_command(wrapper_name="stacks_wrapper") + def create_stack(wrapper_instance, args): + """Copies a specified ZenML stack to a new stack.""" + return wrapper_instance.create_stack(args) + + @self.command(f"{TOOL_MODULE_NAME}.updateStack") + @self.zenml_command(wrapper_name="stacks_wrapper") + def update_stack(wrapper_instance, args): + """Copies a specified ZenML stack to a new stack.""" + return wrapper_instance.update_stack(args) + @self.command(f"{TOOL_MODULE_NAME}.listComponents") @self.zenml_command(wrapper_name="stacks_wrapper") def list_components(wrapper_instance, args): diff --git a/bundled/tool/zenml_wrappers.py b/bundled/tool/zenml_wrappers.py index 11c52b18..7e1f5897 100644 --- a/bundled/tool/zenml_wrappers.py +++ b/bundled/tool/zenml_wrappers.py @@ -13,7 +13,7 @@ """This module provides wrappers for ZenML configuration and operations.""" import pathlib -from typing import Any, Tuple, Union, List, Optional +from typing import Any, Tuple, Union, List, Optional, Dict from zenml_grapher import Grapher from type_hints import ( GraphResponse, @@ -725,6 +725,29 @@ def copy_stack(self, args) -> dict: self.StackComponentValidationError, ) as e: return {"error": str(e)} + + def create_stack(self, args: Tuple[str, Dict[str, str]]) -> Dict[str, str]: + [name, components] = args + + try: + self.client.create_stack(name, components) + return {"message": f"Stack {name} successfully created"} + except self.ZenMLBaseException as e: + return {"error": str(e)} + + def update_stack(self, args: Tuple[str, str, Dict[str, List[str]]]) -> Dict[str, str]: + [id, name, components] = args + + try: + old = self.client.get_stack(id) + if old.name == name: + self.client.update_stack(name_id_or_prefix=id, component_updates=components) + else: + self.client.update_stack(name_id_or_prefix=id, name=name, component_updates=components) + + return {"message": f"Stack {name} successfully updated."} + except self.ZenMLBaseException as e: + return {"error": str(e)} def list_components(self, args: Tuple[int, int, Union[str, None]]) -> Union[ListComponentsResponse,ErrorResponse]: if len(args) < 2: diff --git a/package.json b/package.json index 9193efa1..54aeefd1 100644 --- a/package.json +++ b/package.json @@ -185,8 +185,8 @@ "category": "ZenML Stacks" }, { - "command": "zenml.renameStack", - "title": "Rename Stack", + "command": "zenml.updateStack", + "title": "Update Stack", "icon": "$(edit)", "category": "ZenML Stacks" }, @@ -392,7 +392,7 @@ }, { "when": "stackCommandsRegistered && view == zenmlStackView && viewItem == stack", - "command": "zenml.renameStack", + "command": "zenml.updateStack", "group": "inline@2" }, { diff --git a/resources/stacks-form/stacks.css b/resources/stacks-form/stacks.css index 870fd0ac..36aa1736 100644 --- a/resources/stacks-form/stacks.css +++ b/resources/stacks-form/stacks.css @@ -77,3 +77,27 @@ p { align-items: center; margin: 10px; } + +.loader { + width: 20px; + height: 20px; + border: 5px solid #fff; + border-bottom-color: #ff3d00; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.hidden { + display: none; +} diff --git a/resources/stacks-form/stacks.js b/resources/stacks-form/stacks.js index e69de29b..df647891 100644 --- a/resources/stacks-form/stacks.js +++ b/resources/stacks-form/stacks.js @@ -0,0 +1,87 @@ +document.querySelector('input[name="orchestrator"]').toggleAttribute('required'); +document.querySelector('input[name="artifact_store"]').toggleAttribute('required'); + +const form = document.querySelector('form'); +const submit = document.querySelector('input[type="submit"]'); +const spinner = document.querySelector('.loader'); +const previousValues = {}; +let id = undefined; +let mode = 'create'; + +form.addEventListener('click', evt => { + const target = evt.target; + + let input = null; + if (target instanceof HTMLLabelElement) { + input = document.getElementById(target.htmlFor); + } + + if (target instanceof HTMLInputElement && target.type === 'radio') { + input = target; + } + if (!input) { + return; + } + + const value = input.value; + const name = input.name; + if (previousValues[name] === value) { + delete previousValues[name]; + input.checked = false; + } else { + previousValues[name] = value; + } +}); + +(() => { + const vscode = acquireVsCodeApi(); + + form.addEventListener('submit', evt => { + evt.preventDefault(); + submit.disabled = true; + spinner.classList.remove('hidden'); + const data = Object.fromEntries(new FormData(evt.target)); + + if (id) { + data.id = id; + } + + vscode.postMessage({ + command: mode, + data, + }); + }); +})(); + +const title = document.querySelector('h2'); +const nameInput = document.querySelector('input[name="name"]'); + +window.addEventListener('message', evt => { + const message = evt.data; + + switch (message.command) { + case 'create': + mode = 'create'; + title.innerText = 'Create Stack'; + id = undefined; + form.reset(); + break; + + case 'update': + mode = 'update'; + title.innerText = 'Update Stack'; + id = message.data.id; + nameInput.value = message.data.name; + Object.entries(message.data.components).forEach(([type, id]) => { + const input = document.querySelector(`[name="${type}"][value="${id}"]`); + input.checked = true; + previousValues[type] = id; + }); + break; + + case 'fail': + spinner.classList.add('hidden'); + submit.disabled = false; + break; + } +}); diff --git a/src/commands/stack/StackForm.ts b/src/commands/stack/StackForm.ts index 4b188ef6..b354e235 100644 --- a/src/commands/stack/StackForm.ts +++ b/src/commands/stack/StackForm.ts @@ -17,6 +17,9 @@ import { handlebars } from 'hbs'; import Panels from '../../common/panels'; import { getAllFlavors, getAllStackComponents } from '../../common/api'; import { Flavor, StackComponent } from '../../types/StackTypes'; +import { LSClient } from '../../services/LSClient'; +import { StackDataProvider } from '../../views/activityBar'; +import { traceError, traceInfo } from '../../common/log/logging'; type MixedComponent = { name: string; id: string; url: string }; @@ -61,12 +64,22 @@ export default class StackForm extends WebviewBase { this.template = handlebars.compile(this.produceTemplate()); } - public async display() { + public async createForm() { + const panel = await this.display(); + panel.webview.postMessage({ command: 'create' }); + } + + public async updateForm(id: string, name: string, components: { [type: string]: string }) { + const panel = await this.display(); + panel.webview.postMessage({ command: 'update', data: { id, name, components } }); + } + + private async display(): Promise { const panels = Panels.getInstance(); const existingPanel = panels.getPanel('stack-form'); if (existingPanel) { existingPanel.reveal(); - return; + return existingPanel; } const panel = panels.createPanel('stack-form', 'Stack Form', { @@ -76,6 +89,106 @@ export default class StackForm extends WebviewBase { }); await this.renderForm(panel); + this.attachListener(panel); + return panel; + } + + private attachListener(panel: vscode.WebviewPanel) { + panel.webview.onDidReceiveMessage( + async (message: { command: string; data: { [key: string]: string } }) => { + let success = false; + const data = message.data; + const name = data.name; + delete data.name; + + switch (message.command) { + case 'create': + success = await this.createStack(name, data); + break; + case 'update': + const id = data.id; + delete data.id; + const updateData = Object.fromEntries( + Object.entries(data).map(([type, id]) => [type, [id]]) + ); + success = await this.updateStack(id, name, updateData); + break; + } + + if (!success) { + panel.webview.postMessage({ command: 'fail' }); + return; + } + + panel.dispose(); + StackDataProvider.getInstance().refresh(); + } + ); + } + + private async createStack( + name: string, + components: { [type: string]: string } + ): Promise { + const lsClient = LSClient.getInstance(); + try { + const resp = await lsClient.sendLsClientRequest('createStack', [name, components]); + + if ('error' in resp) { + vscode.window.showErrorMessage(`Unable to create stack: "${resp.error}"`); + console.error(resp.error); + traceError(resp.error); + return false; + } + + traceInfo(resp.message); + } catch (e) { + vscode.window.showErrorMessage(`Unable to create stack: "${e}"`); + console.error(e); + traceError(e); + return false; + } + + return true; + } + + private async updateStack( + id: string, + name: string, + components: { [key: string]: string[] } + ): Promise { + const lsClient = LSClient.getInstance(); + try { + const types = await lsClient.sendLsClientRequest('getComponentTypes'); + if (!Array.isArray(types)) { + throw new Error('Could not get Component Types from LS Server'); + } + + // adding missing types to components object, in case we removed that type. + types.forEach(type => { + if (!components[type]) { + components[type] = []; + } + }); + + const resp = await lsClient.sendLsClientRequest('updateStack', [id, name, components]); + + if ('error' in resp) { + vscode.window.showErrorMessage(`Unable to update stack: "${resp.error}"`); + console.error(resp.error); + traceError(resp.error); + return false; + } + + traceInfo(resp.message); + } catch (e) { + vscode.window.showErrorMessage(`Unable to update stack: "${e}"`); + console.error(e); + traceError(e); + return false; + } + + return true; } private async renderForm(panel: vscode.WebviewPanel) { @@ -124,7 +237,7 @@ export default class StackForm extends WebviewBase {

Create Stack

- + {{#each options}}

{{capitalize @key}}

@@ -138,6 +251,7 @@ export default class StackForm extends WebviewBase { {{/each}}
+
diff --git a/src/commands/stack/cmds.ts b/src/commands/stack/cmds.ts index f8f53779..6e347a34 100644 --- a/src/commands/stack/cmds.ts +++ b/src/commands/stack/cmds.ts @@ -11,7 +11,7 @@ // or implied.See the License for the specific language governing // permissions and limitations under the License. import * as vscode from 'vscode'; -import { StackDataProvider, StackTreeItem } from '../../views/activityBar'; +import { StackComponentTreeItem, StackDataProvider, StackTreeItem } from '../../views/activityBar'; import ZenMLStatusBar from '../../views/statusBar'; import { getStackDashboardUrl, switchActiveStack } from './utils'; import { LSClient } from '../../services/LSClient'; @@ -160,10 +160,6 @@ const setActiveStack = async (node: StackTreeItem): Promise => { ); }; -const createStack = () => { - StackForm.getInstance().display(); -}; - /** * Opens the selected stack in the ZenML Dashboard in the browser * @@ -185,6 +181,24 @@ const goToStackUrl = (node: StackTreeItem) => { } }; +const createStack = () => { + StackForm.getInstance().createForm(); +}; + +const updateStack = async (node: StackTreeItem) => { + const { id, label: name } = node; + const components: { [type: string]: string } = {}; + + node.children?.forEach(child => { + if (child instanceof StackComponentTreeItem) { + const { type, id } = (child as StackComponentTreeItem).component; + components[type] = id; + } + }); + + StackForm.getInstance().updateForm(id, name, components); +}; + export const stackCommands = { refreshStackView, refreshActiveStack, @@ -193,4 +207,5 @@ export const stackCommands = { setActiveStack, goToStackUrl, createStack, + updateStack, }; diff --git a/src/commands/stack/registry.ts b/src/commands/stack/registry.ts index b6a4e38a..d2741337 100644 --- a/src/commands/stack/registry.ts +++ b/src/commands/stack/registry.ts @@ -35,9 +35,9 @@ export const registerStackCommands = (context: ExtensionContext) => { 'zenml.refreshActiveStack', async () => await stackCommands.refreshActiveStack() ), - registerCommand( - 'zenml.createStack', - async () => await stackCommands.createStack() + registerCommand('zenml.createStack', async () => stackCommands.createStack()), + registerCommand('zenml.updateStack', async (node: StackTreeItem) => + stackCommands.updateStack(node) ), registerCommand( 'zenml.renameStack', From 0d43baf85b22b3b547afcf04566f8a6cc1b17a86 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Sat, 20 Jul 2024 23:38:18 -0400 Subject: [PATCH 27/45] feat: Added create_component to zenml wrapper --- bundled/tool/lsp_zenml.py | 6 ++++++ bundled/tool/type_hints.py | 8 ++++++-- bundled/tool/zenml_wrappers.py | 22 ++++++++++++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/bundled/tool/lsp_zenml.py b/bundled/tool/lsp_zenml.py index 310276c8..4edb30cf 100644 --- a/bundled/tool/lsp_zenml.py +++ b/bundled/tool/lsp_zenml.py @@ -290,6 +290,12 @@ def update_stack(wrapper_instance, args): """Copies a specified ZenML stack to a new stack.""" return wrapper_instance.update_stack(args) + @self.command(f"{TOOL_MODULE_NAME}.createComponent") + @self.zenml_command(wrapper_name="stacks_wrapper") + def create_component(wrapper_instance, args): + """Get paginated stack components from ZenML""" + return wrapper_instance.create_component(args) + @self.command(f"{TOOL_MODULE_NAME}.listComponents") @self.zenml_command(wrapper_name="stacks_wrapper") def list_components(wrapper_instance, args): diff --git a/bundled/tool/type_hints.py b/bundled/tool/type_hints.py index 7d6247a6..ed58d462 100644 --- a/bundled/tool/type_hints.py +++ b/bundled/tool/type_hints.py @@ -102,12 +102,16 @@ class Flavor(TypedDict): name: str type: str logo_url: str - description: str config_schema: Dict[str, Any] + docs_url: Optional[str] + sdk_docs_url: Optional[str] + connector_type: Optional[str] + connector_resource_type: Optional[str] + connector_resource_id_attr: Optional[str] class ListFlavorsResponse(TypedDict): index: int max_size: int total_pages: int total: int - items: List[StackComponent] \ No newline at end of file + items: List[Flavor] \ No newline at end of file diff --git a/bundled/tool/zenml_wrappers.py b/bundled/tool/zenml_wrappers.py index 7e1f5897..a193c2e2 100644 --- a/bundled/tool/zenml_wrappers.py +++ b/bundled/tool/zenml_wrappers.py @@ -748,6 +748,16 @@ def update_stack(self, args: Tuple[str, str, Dict[str, List[str]]]) -> Dict[str, return {"message": f"Stack {name} successfully updated."} except self.ZenMLBaseException as e: return {"error": str(e)} + + def create_component(self, args: Tuple[str, str, str, Dict[str, str]]) -> Dict[str, str]: + [component_type, flavor, name, configuration] = args + + try: + self.client.create_stack_components(name, flavor, component_type, configuration) + + return {"message": f"Stack Component {name} successfully created"} + except self.ZenMLBaseException as e: + return {"error": str(e)} def list_components(self, args: Tuple[int, int, Union[str, None]]) -> Union[ListComponentsResponse,ErrorResponse]: if len(args) < 2: @@ -781,8 +791,11 @@ def list_components(self, args: Tuple[int, int, Union[str, None]]) -> Union[List except self.ZenMLBaseException as e: return {"error": f"Failed to retrieve list of stack components: {str(e)}"} - def get_component_types(self) -> List[str]: - return self.StackComponentType.values() + def get_component_types(self) -> Union[List[str], ErrorResponse]: + try: + return self.StackComponentType.values() + except self.ZenMLBaseException as e: + return {"error": f"Failed to retrieve list of component types: {str(e)}"} def list_flavors(self, args: Tuple[int, int, Optional[str]]) -> Union[ListFlavorsResponse, ErrorResponse]: if len(args) < 2: @@ -809,6 +822,11 @@ def list_flavors(self, args: Tuple[int, int, Optional[str]]) -> Union[ListFlavor "type": flavor.body.type, "logo_url": flavor.body.logo_url, "config_schema": flavor.metadata.config_schema, + "docs_url": flavor.metadata.docs_url, + "sdk_docs_url": flavor.metadata.sdk_docs_url, + "connector_type": flavor.metadata.connector_type, + "connector_resource_type": flavor.metadata.connector_resource_type, + "connector_resource_id_attr": flavor.metadata.connector_resource_id_attr, } for flavor in flavors.items ] } From 60a845cbe4aff16083c1437f985dafa4cb273a32 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Sun, 21 Jul 2024 00:23:31 -0400 Subject: [PATCH 28/45] feat: createComponent command added to component view --- package.json | 15 ++- resources/components-form/components.css | 0 resources/components-form/components.js | 1 + resources/stacks-form/stacks.js | 5 +- src/commands/components/ComponentsForm.ts | 128 ++++++++++++++++++++++ src/commands/components/cmds.ts | 39 +++++++ src/commands/components/registry.ts | 4 + src/common/api.ts | 5 + src/types/StackTypes.ts | 20 +++- 9 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 resources/components-form/components.css create mode 100644 resources/components-form/components.js create mode 100644 src/commands/components/ComponentsForm.ts diff --git a/package.json b/package.json index 54aeefd1..9d7c87b6 100644 --- a/package.json +++ b/package.json @@ -208,6 +208,12 @@ "icon": "$(refresh)", "category": "ZenML Components" }, + { + "command": "zenml.createComponent", + "title": "Create Component", + "icon": "$(add)", + "category": "ZenML Components" + }, { "command": "zenml.copyStack", "title": "Copy Stack", @@ -360,14 +366,19 @@ }, { "when": "componentCommandsRegistered && view == zenmlComponentView", - "command": "zenml.setComponentItemsPerPage", + "command": "zenml.createComponent", "group": "navigation@1" }, { "when": "componentCommandsRegistered && view == zenmlComponentView", - "command": "zenml.refreshComponentView", + "command": "zenml.setComponentItemsPerPage", "group": "navigation@2" }, + { + "when": "componentCommandsRegistered && view == zenmlComponentView", + "command": "zenml.refreshComponentView", + "group": "navigation@3" + }, { "when": "pipelineCommandsRegistered && view == zenmlPipelineView", "command": "zenml.setPipelineRunsPerPage", diff --git a/resources/components-form/components.css b/resources/components-form/components.css new file mode 100644 index 00000000..e69de29b diff --git a/resources/components-form/components.js b/resources/components-form/components.js new file mode 100644 index 00000000..641bee38 --- /dev/null +++ b/resources/components-form/components.js @@ -0,0 +1 @@ +console.log('working'); diff --git a/resources/stacks-form/stacks.js b/resources/stacks-form/stacks.js index df647891..0143e441 100644 --- a/resources/stacks-form/stacks.js +++ b/resources/stacks-form/stacks.js @@ -4,7 +4,7 @@ document.querySelector('input[name="artifact_store"]').toggleAttribute('required const form = document.querySelector('form'); const submit = document.querySelector('input[type="submit"]'); const spinner = document.querySelector('.loader'); -const previousValues = {}; +let previousValues = {}; let id = undefined; let mode = 'create'; @@ -64,6 +64,7 @@ window.addEventListener('message', evt => { mode = 'create'; title.innerText = 'Create Stack'; id = undefined; + previousValues = {}; form.reset(); break; @@ -72,10 +73,10 @@ window.addEventListener('message', evt => { title.innerText = 'Update Stack'; id = message.data.id; nameInput.value = message.data.name; + previousValues = message.data.components; Object.entries(message.data.components).forEach(([type, id]) => { const input = document.querySelector(`[name="${type}"][value="${id}"]`); input.checked = true; - previousValues[type] = id; }); break; diff --git a/src/commands/components/ComponentsForm.ts b/src/commands/components/ComponentsForm.ts new file mode 100644 index 00000000..aa5c9c9c --- /dev/null +++ b/src/commands/components/ComponentsForm.ts @@ -0,0 +1,128 @@ +// 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 * as vscode from 'vscode'; +import WebviewBase from '../../common/WebviewBase'; +import { handlebars } from 'hbs'; +import Panels from '../../common/panels'; +import { Flavor } from '../../types/StackTypes'; +import { LSClient } from '../../services/LSClient'; +import { traceError, traceInfo } from '../../common/log/logging'; + +const ROOT_PATH = ['resources', 'components-form']; +const CSS_FILE = 'components.css'; +const JS_FILE = 'components.js'; + +export default class ComponentForm extends WebviewBase { + private static instance: ComponentForm | null = null; + + private root: vscode.Uri; + private javaScript: vscode.Uri; + private css: vscode.Uri; + private template: HandlebarsTemplateDelegate; + + public static getInstance(): ComponentForm { + if (!ComponentForm.instance) { + ComponentForm.instance = new ComponentForm(); + } + + return ComponentForm.instance; + } + + constructor() { + super(); + + if (WebviewBase.context === null) { + throw new Error('Extension Context Not Propagated'); + } + + this.root = vscode.Uri.joinPath(WebviewBase.context.extensionUri, ...ROOT_PATH); + this.javaScript = vscode.Uri.joinPath(this.root, JS_FILE); + this.css = vscode.Uri.joinPath(this.root, CSS_FILE); + + this.template = handlebars.compile(this.produceTemplate()); + } + + public async createForm(flavor: Flavor) { + const panel = await this.getPanel(); + panel.webview.html = this.template({ + type: flavor.type, + flavor: flavor.name, + description: flavor.config_schema.description, + docs_url: flavor.docs_url, + sdk_docs_url: flavor.sdk_docs_url, + js: panel.webview.asWebviewUri(this.javaScript), + css: panel.webview.asWebviewUri(this.css), + }); + } + + private async getPanel(): Promise { + const panels = Panels.getInstance(); + const existingPanel = panels.getPanel('stack-form'); + if (existingPanel) { + existingPanel.reveal(); + return existingPanel; + } + + const panel = panels.createPanel('stack-form', 'Stack Form', { + enableForms: true, + enableScripts: true, + retainContextWhenHidden: true, + }); + + this.attachListener(panel); + return panel; + } + + private attachListener(panel: vscode.WebviewPanel) {} + + private produceTemplate(): string { + return ` + + + + + + + + Stack Form + + +

Create {{type}} Stack Component ({{flavor}})

+
+
{{description}}
+
+ {{#if docs_url}} + Documentation + {{/if}} + + {{#if sdk_docs_url}} + SDK Documentation + {{/if}} +
+
+
+ + + + + +
+
+
+ + + + `; + } +} diff --git a/src/commands/components/cmds.ts b/src/commands/components/cmds.ts index 8e33eb3d..c6e890ad 100644 --- a/src/commands/components/cmds.ts +++ b/src/commands/components/cmds.ts @@ -17,6 +17,9 @@ import { LSClient } from '../../services/LSClient'; import { showInformationMessage } from '../../utils/notifications'; import Panels from '../../common/panels'; import { ComponentDataProvider } from '../../views/activityBar/componentView/ComponentDataProvider'; +import { ComponentTypesResponse, FlavorListResponse } from '../../types/StackTypes'; +import { getFlavorsOfType } from '../../common/api'; +import ComponentForm from './ComponentsForm'; const refreshComponentView = async () => { vscode.window.withProgress( @@ -31,6 +34,42 @@ const refreshComponentView = async () => { ); }; +const createComponent = async () => { + const lsClient = LSClient.getInstance(); + const types = await lsClient.sendLsClientRequest('getComponentTypes'); + + if ('error' in types) { + return; // todo: real error handling. + } + + const type = await vscode.window.showQuickPick(types, { + title: 'What type of component to create?', + }); + if (!type) { + return; + } + + const flavors = await getFlavorsOfType(type); + if ('error' in flavors) { + return; // todo: real Error handling + } + + const flavorNames = flavors.map(flavor => flavor.name); + const selectedFlavor = await vscode.window.showQuickPick(flavorNames, { + title: `What flavor of a ${type} component to create?`, + }); + if (!selectedFlavor) { + return; + } + + const flavor = flavors.find(flavor => selectedFlavor === flavor.name); + if (!flavor) { + return; + } + ComponentForm.getInstance().createForm(flavor); +}; + export const componentCommands = { refreshComponentView, + createComponent, }; diff --git a/src/commands/components/registry.ts b/src/commands/components/registry.ts index 392e04fa..c54f3faa 100644 --- a/src/commands/components/registry.ts +++ b/src/commands/components/registry.ts @@ -33,6 +33,10 @@ export const registerComponentCommands = (context: ExtensionContext) => { 'zenml.refreshComponentView', async () => await componentCommands.refreshComponentView() ), + registerCommand( + 'zenml.createComponent', + async () => await componentCommands.createComponent() + ), registerCommand( 'zenml.nextComponentPage', async () => await componentDataProvider.goToNextPage() diff --git a/src/common/api.ts b/src/common/api.ts index 64b7d492..eed23ee3 100644 --- a/src/common/api.ts +++ b/src/common/api.ts @@ -33,6 +33,11 @@ export const getAllFlavors = async (): Promise => { return flavors; }; +export const getFlavorsOfType = async (type: string): Promise => { + const flavors = await getAllFlavors(); + return flavors.filter(flavor => flavor.type === type); +}; + export const getAllStackComponents = async (): Promise<{ [type: string]: StackComponent[]; }> => { diff --git a/src/types/StackTypes.ts b/src/types/StackTypes.ts index 930fec6a..a9244cc2 100644 --- a/src/types/StackTypes.ts +++ b/src/types/StackTypes.ts @@ -62,8 +62,12 @@ interface Flavor { name: string; type: string; logo_url: string; - description: string; config_schema: { [key: string]: any }; + docs_url: string | null; + sdk_docs_url: string | null; + connector_type: string | null; + connector_resource_type: string | null; + connector_resource_id_attr: string | null; } interface FlavorListData { @@ -76,4 +80,16 @@ interface FlavorListData { export type FlavorListResponse = FlavorListData | ErrorMessageResponse | VersionMismatchError; -export { Stack, Components, StackComponent, StacksData, ComponentsListData, Flavor }; +type ComponentTypes = string[]; + +export type ComponentTypesResponse = ComponentTypes | VersionMismatchError | ErrorMessageResponse; + +export { + Stack, + Components, + StackComponent, + StacksData, + ComponentsListData, + Flavor, + ComponentTypes, +}; From 453d7ade70fd76c0445f3c2f36adbb8ed49a376b Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Mon, 22 Jul 2024 16:46:38 -0400 Subject: [PATCH 29/45] feat: create component form is functional --- bundled/tool/zenml_wrappers.py | 2 +- resources/components-form/components.css | 76 +++++++ resources/components-form/components.js | 98 ++++++++- src/commands/components/ComponentsForm.ts | 244 ++++++++++++++++++++-- src/commands/stack/StackForm.ts | 5 +- 5 files changed, 397 insertions(+), 28 deletions(-) diff --git a/bundled/tool/zenml_wrappers.py b/bundled/tool/zenml_wrappers.py index a193c2e2..1667d8f2 100644 --- a/bundled/tool/zenml_wrappers.py +++ b/bundled/tool/zenml_wrappers.py @@ -753,7 +753,7 @@ def create_component(self, args: Tuple[str, str, str, Dict[str, str]]) -> Dict[s [component_type, flavor, name, configuration] = args try: - self.client.create_stack_components(name, flavor, component_type, configuration) + self.client.create_stack_component(name, flavor, component_type, configuration) return {"message": f"Stack Component {name} successfully created"} except self.ZenMLBaseException as e: diff --git a/resources/components-form/components.css b/resources/components-form/components.css index e69de29b..f7b5159c 100644 --- a/resources/components-form/components.css +++ b/resources/components-form/components.css @@ -0,0 +1,76 @@ +.container { + max-width: 600px; + margin: auto; +} + +.block { + padding: 10px 10px 5px 10px; + border: 2px var(--vscode-editor-foreground) solid; + margin-bottom: 10px; +} + +.logo { + float: right; + max-width: 100px; +} + +.docs { + clear: both; + display: flex; + justify-content: space-around; + padding: 5px 0px; +} + +.button, +button { + padding: 2px 5px; + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); + border: 2px var(--vscode-editor-foreground) solid; + border-radius: 10px; +} + +.field { + margin-bottom: 10px; +} + +.value { + width: 100%; + box-sizing: border-box; + padding-left: 20px; +} + +.value > input, +textarea { + width: 100%; +} + +.center { + display: flex; + align-items: center; + justify-content: center; +} + +.loader { + width: 20px; + height: 20px; + border: 5px solid #fff; + border-bottom-color: #ff3d00; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.hidden { + display: none; +} diff --git a/resources/components-form/components.js b/resources/components-form/components.js index 641bee38..59e3af8e 100644 --- a/resources/components-form/components.js +++ b/resources/components-form/components.js @@ -1 +1,97 @@ -console.log('working'); +const form = document.querySelector('form'); +const submit = document.querySelector('input[type="submit"]'); +const spinner = document.querySelector('.loader'); + +let mode = 'create'; +let type = ''; +let flavor = ''; +let id = ''; + +const inputs = {}; +document.querySelectorAll('.field > input, textarea').forEach(element => { + inputs[element.id] = element; + if (element instanceof HTMLTextAreaElement) { + element.addEventListener('input', evt => { + try { + JSON.parse(evt.target.value); + } catch { + element.setCustomValidity('Invalid JSON value'); + element.reportValidity(); + return; + } + element.setCustomValidity(''); + }); + } +}); + +form.addEventListener('click', evt => { + const target = evt.target; + if (!(target instanceof HTMLButtonElement)) { + return; + } + + evt.preventDefault(); + + const current = target.textContent; + target.textContent = current === '+' ? '-' : '+'; + const fieldName = target.dataset.id; + const field = document.getElementById(fieldName); + field.classList.toggle('hidden'); +}); + +(() => { + const vscode = acquireVsCodeApi(); + + form.addEventListener('submit', evt => { + evt.preventDefault(); + + const data = Object.fromEntries(new FormData(form)); + + for (const id in inputs) { + if (inputs[id].classList.contains('hidden')) { + delete data[id]; + continue; + } + + if (inputs[id] instanceof HTMLTextAreaElement) { + data[id] = inputs[id].textContent; + } + } + + data.flavor = flavor; + data.type = type; + + submit.disabled = true; + spinner.classList.remove('hidden'); + vscode.postMessage({ + command: mode, + data, + }); + }); +})(); + +window.addEventListener('message', evt => { + const message = evt.data; + + switch (message.command) { + case 'create': + mode = 'create'; + type = message.type; + flavor = message.flavor; + id = ''; + break; + + case 'update': + mode = 'update'; + type = message.type; + flavor = message.flavor; + id = message.id; + // TODO: set fields to data values + break; + + case 'fail': + spinner.classList.add('hidden'); + submit.disabled = false; + break; + } +}); diff --git a/src/commands/components/ComponentsForm.ts b/src/commands/components/ComponentsForm.ts index aa5c9c9c..652fe471 100644 --- a/src/commands/components/ComponentsForm.ts +++ b/src/commands/components/ComponentsForm.ts @@ -18,11 +18,25 @@ import Panels from '../../common/panels'; import { Flavor } from '../../types/StackTypes'; import { LSClient } from '../../services/LSClient'; import { traceError, traceInfo } from '../../common/log/logging'; +import { ComponentDataProvider } from '../../views/activityBar/componentView/ComponentDataProvider'; const ROOT_PATH = ['resources', 'components-form']; const CSS_FILE = 'components.css'; const JS_FILE = 'components.js'; +interface ComponentField { + is_string?: boolean; + is_integer?: boolean; + is_boolean?: boolean; + is_string_object?: boolean; + is_json_object?: boolean; + is_optional?: boolean; + is_required?: boolean; + defaultValue: any; + title: string; + key: string; +} + export default class ComponentForm extends WebviewBase { private static instance: ComponentForm | null = null; @@ -55,26 +69,31 @@ export default class ComponentForm extends WebviewBase { public async createForm(flavor: Flavor) { const panel = await this.getPanel(); + const description = flavor.config_schema.description.replaceAll('\n', '
'); panel.webview.html = this.template({ type: flavor.type, flavor: flavor.name, - description: flavor.config_schema.description, + logo: flavor.logo_url, + description, docs_url: flavor.docs_url, sdk_docs_url: flavor.sdk_docs_url, js: panel.webview.asWebviewUri(this.javaScript), css: panel.webview.asWebviewUri(this.css), + fields: this.toFormFields(flavor.config_schema), }); + + panel.webview.postMessage({ command: 'create', type: flavor.type, flavor: flavor.name }); } private async getPanel(): Promise { const panels = Panels.getInstance(); - const existingPanel = panels.getPanel('stack-form'); + const existingPanel = panels.getPanel('component-form'); if (existingPanel) { existingPanel.reveal(); return existingPanel; } - const panel = panels.createPanel('stack-form', 'Stack Form', { + const panel = panels.createPanel('component-form', 'Component Form', { enableForms: true, enableScripts: true, retainContextWhenHidden: true, @@ -84,7 +103,122 @@ export default class ComponentForm extends WebviewBase { return panel; } - private attachListener(panel: vscode.WebviewPanel) {} + private attachListener(panel: vscode.WebviewPanel) { + panel.webview.onDidReceiveMessage( + async (message: { command: string; data: { [key: string]: string } }) => { + let success = false; + const data = message.data; + const { name, flavor, type, id } = data; + delete data.name; + delete data.type; + delete data.flavor; + delete data.id; + + switch (message.command) { + case 'create': + success = await this.createComponent(name, type, flavor, data); + break; + case 'update': + // TODO + break; + } + + if (!success) { + panel.webview.postMessage({ command: 'fail' }); + return; + } + + panel.dispose(); + ComponentDataProvider.getInstance().refresh(); + } + ); + } + + private async createComponent( + name: string, + type: string, + flavor: string, + data: object + ): Promise { + const lsClient = LSClient.getInstance(); + try { + const resp = await lsClient.sendLsClientRequest('createComponent', [ + type, + flavor, + name, + data, + ]); + + if ('error' in resp) { + vscode.window.showErrorMessage(`Unable to create component: "${resp.error}"`); + console.error(resp.error); + traceError(resp.error); + return false; + } + + traceInfo(resp.message); + } catch (e) { + vscode.window.showErrorMessage(`Unable to create component: "${e}"`); + console.error(e); + traceError(e); + return false; + } + + return true; + } + + private toFormFields(configSchema: { [key: string]: any }) { + const properties = configSchema.properties; + const required = configSchema.required ?? []; + + const converted: Array = []; + for (const key in properties) { + const current: ComponentField = { + key, + title: properties[key].title, + defaultValue: properties[key].default, + }; + converted.push(current); + + if ('anyOf' in properties[key]) { + if (properties[key].anyOf.find((obj: { type: string }) => obj.type === 'null')) { + current.is_optional = true; + } + + if ( + properties[key].anyOf.find( + (obj: { type: string }) => obj.type === 'object' || obj.type === 'array' + ) + ) { + current.is_json_object = true; + } else if (properties[key].anyOf[0].type === 'string') { + current.is_string = true; + } else if (properties[key].anyOf[0].type === 'integer') { + current.is_integer = true; + } else if (properties[key].anyOf[0].type === 'boolean') { + current.is_boolean = true; + } + } + + if (required.includes(key)) { + current.is_required = true; + } + + if (!properties[key].type) { + continue; + } + + current.is_boolean = properties[key].type === 'boolean'; + current.is_string = properties[key].type === 'string'; + current.is_integer = properties[key].type === 'integer'; + if (properties[key].type === 'object' || properties[key].type === 'array') { + current.is_json_object = true; + current.defaultValue = JSON.stringify(properties[key].default); + } + } + + return converted; + } private produceTemplate(): string { return ` @@ -98,28 +232,92 @@ export default class ComponentForm extends WebviewBase { Stack Form -

Create {{type}} Stack Component ({{flavor}})

-
-
{{description}}
-
- {{#if docs_url}} - Documentation - {{/if}} - - {{#if sdk_docs_url}} - SDK Documentation - {{/if}} +
+

Create {{type}} Stack Component ({{flavor}})

+ +
+
{{{description}}}
+
+ {{#if docs_url}} + Documentation + {{/if}} + + {{#if sdk_docs_url}} + SDK Documentation + {{/if}} +
-
-
- - + +
+
+ +
+
+ +
+
+ {{#each fields}} +
+
+ + {{#if is_optional}} + + {{/if}} +
- -
- -
+
+ {{#if is_string}} + + {{/if}} + + {{#if is_boolean}} + + {{/if}} + + {{#if is_integer}} + + {{/if}} + + {{#if is_json_object}} + + {{/if}} +
+
+ {{/each}} +
+ +
+
diff --git a/src/commands/stack/StackForm.ts b/src/commands/stack/StackForm.ts index b354e235..16642180 100644 --- a/src/commands/stack/StackForm.ts +++ b/src/commands/stack/StackForm.ts @@ -98,16 +98,15 @@ export default class StackForm extends WebviewBase { async (message: { command: string; data: { [key: string]: string } }) => { let success = false; const data = message.data; - const name = data.name; + const { name, id } = data; delete data.name; + delete data.id; switch (message.command) { case 'create': success = await this.createStack(name, data); break; case 'update': - const id = data.id; - delete data.id; const updateData = Object.fromEntries( Object.entries(data).map(([type, id]) => [type, [id]]) ); From d076333e85a62c546f4a88b087f4112c6363c3a8 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Tue, 23 Jul 2024 00:58:11 -0400 Subject: [PATCH 30/45] feat: improvements to validating and data types when submititing new components --- resources/components-form/components.js | 17 +++++++++++++++-- src/commands/components/ComponentsForm.ts | 10 ++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/resources/components-form/components.js b/resources/components-form/components.js index 59e3af8e..97dca70c 100644 --- a/resources/components-form/components.js +++ b/resources/components-form/components.js @@ -13,7 +13,12 @@ document.querySelectorAll('.field > input, textarea').forEach(element => { if (element instanceof HTMLTextAreaElement) { element.addEventListener('input', evt => { try { - JSON.parse(evt.target.value); + const val = JSON.parse(evt.target.value); + if (evt.target.dataset.array && !Array.isArray(val)) { + element.setCustomValidity('Must be an array'); + element.reportValidity(); + return; + } } catch { element.setCustomValidity('Invalid JSON value'); element.reportValidity(); @@ -54,7 +59,15 @@ form.addEventListener('click', evt => { } if (inputs[id] instanceof HTMLTextAreaElement) { - data[id] = inputs[id].textContent; + data[id] = JSON.parse(inputs[id].value); + } + + if (inputs[id].type === 'checkbox') { + data[id] = !!inputs[id].checked; + } + + if (inputs[id].type === 'number') { + data[id] = Number(inputs[id].value); } } diff --git a/src/commands/components/ComponentsForm.ts b/src/commands/components/ComponentsForm.ts index 652fe471..a5ac424b 100644 --- a/src/commands/components/ComponentsForm.ts +++ b/src/commands/components/ComponentsForm.ts @@ -30,6 +30,7 @@ interface ComponentField { is_boolean?: boolean; is_string_object?: boolean; is_json_object?: boolean; + is_array?: boolean; is_optional?: boolean; is_required?: boolean; defaultValue: any; @@ -215,6 +216,10 @@ export default class ComponentForm extends WebviewBase { current.is_json_object = true; current.defaultValue = JSON.stringify(properties[key].default); } + + if (properties[key].type === 'array') { + current.is_array = true; + } } return converted; @@ -250,7 +255,7 @@ export default class ComponentForm extends WebviewBase {
- +
@@ -305,8 +310,9 @@ export default class ComponentForm extends WebviewBase { {{#if is_json_object}} From 109551e71087f212e4e03cbbac37a19d9d42c917 Mon Sep 17 00:00:00 2001 From: Christopher Perkins Date: Tue, 23 Jul 2024 12:53:20 -0400 Subject: [PATCH 31/45] feat: added ability to update stack components --- bundled/tool/lsp_zenml.py | 12 +++- bundled/tool/type_hints.py | 1 + bundled/tool/zenml_wrappers.py | 16 +++++ package.json | 43 ++++++++----- resources/components-form/components.js | 41 ++++++++++++- src/commands/components/ComponentsForm.ts | 70 +++++++++++++++++++-- src/commands/components/cmds.ts | 75 +++++++++++++++-------- src/commands/components/registry.ts | 5 ++ src/common/api.ts | 11 ++++ src/common/panels.ts | 10 ++- src/types/StackTypes.ts | 1 + 11 files changed, 229 insertions(+), 56 deletions(-) diff --git a/bundled/tool/lsp_zenml.py b/bundled/tool/lsp_zenml.py index 4edb30cf..1926c317 100644 --- a/bundled/tool/lsp_zenml.py +++ b/bundled/tool/lsp_zenml.py @@ -293,9 +293,15 @@ def update_stack(wrapper_instance, args): @self.command(f"{TOOL_MODULE_NAME}.createComponent") @self.zenml_command(wrapper_name="stacks_wrapper") def create_component(wrapper_instance, args): - """Get paginated stack components from ZenML""" + """Creates a Zenml stack component""" return wrapper_instance.create_component(args) + @self.command(f"{TOOL_MODULE_NAME}.updateComponent") + @self.zenml_command(wrapper_name="stacks_wrapper") + def update_component(wrapper_instance, args): + """Updates a ZenML stack component""" + return wrapper_instance.update_component(args) + @self.command(f"{TOOL_MODULE_NAME}.listComponents") @self.zenml_command(wrapper_name="stacks_wrapper") def list_components(wrapper_instance, args): @@ -305,13 +311,13 @@ def list_components(wrapper_instance, args): @self.command(f"{TOOL_MODULE_NAME}.getComponentTypes") @self.zenml_command(wrapper_name="stacks_wrapper") def get_component_types(wrapper_instance, args): - """Get paginated stack components from ZenML""" + """Get list of component types from ZenML""" return wrapper_instance.get_component_types() @self.command(f"{TOOL_MODULE_NAME}.listFlavors") @self.zenml_command(wrapper_name="stacks_wrapper") def list_flavors(wrapper_instance, args): - """Get paginated stack components from ZenML""" + """Get list of component flavors from ZenML""" return wrapper_instance.list_flavors(args) @self.command(f"{TOOL_MODULE_NAME}.getPipelineRuns") diff --git a/bundled/tool/type_hints.py b/bundled/tool/type_hints.py index ed58d462..ff2332be 100644 --- a/bundled/tool/type_hints.py +++ b/bundled/tool/type_hints.py @@ -89,6 +89,7 @@ class StackComponent(TypedDict): name: str flavor: str type: str + config: Dict[str, Any] class ListComponentsResponse(TypedDict): index: int diff --git a/bundled/tool/zenml_wrappers.py b/bundled/tool/zenml_wrappers.py index 1667d8f2..4cd321a3 100644 --- a/bundled/tool/zenml_wrappers.py +++ b/bundled/tool/zenml_wrappers.py @@ -758,7 +758,22 @@ def create_component(self, args: Tuple[str, str, str, Dict[str, str]]) -> Dict[s return {"message": f"Stack Component {name} successfully created"} except self.ZenMLBaseException as e: return {"error": str(e)} + + def update_component(self, args: Tuple[str, str, str, Dict[str, str]]) -> Dict[str, str]: + [id, component_type, name, configuration] = args + + try: + old = self.client.get_stack_component(component_type, id) + + if old.name == name: + name = None + self.client.update_stack_component(id, component_type, name=name, configuration=configuration) + + return {"message": f"Stack Component {name} successfully updated"} + except self.ZenMLBaseException as e: + return {"error": str(e)} + def list_components(self, args: Tuple[int, int, Union[str, None]]) -> Union[ListComponentsResponse,ErrorResponse]: if len(args) < 2: return {"error": "Insufficient arguments provided."} @@ -784,6 +799,7 @@ def list_components(self, args: Tuple[int, int, Union[str, None]]) -> Union[List "name": item.name, "flavor": item.body.flavor, "type": item.body.type, + "config": item.metadata.configuration, } for item in components.items ], diff --git a/package.json b/package.json index 9d7c87b6..d59b2d65 100644 --- a/package.json +++ b/package.json @@ -184,6 +184,12 @@ "title": "Get Active Stack", "category": "ZenML Stacks" }, + { + "command": "zenml.createStack", + "title": "Create Stack", + "icon": "$(add)", + "category": "ZenML Stacks" + }, { "command": "zenml.updateStack", "title": "Update Stack", @@ -196,6 +202,18 @@ "icon": "$(check)", "category": "ZenML Stacks" }, + { + "command": "zenml.copyStack", + "title": "Copy Stack", + "icon": "$(copy)", + "category": "ZenML Stacks" + }, + { + "command": "zenml.goToStackUrl", + "title": "Go to URL", + "icon": "$(globe)", + "category": "ZenML Stacks" + }, { "command": "zenml.setComponentItemsPerPage", "title": "Set Components Per Page", @@ -215,16 +233,10 @@ "category": "ZenML Components" }, { - "command": "zenml.copyStack", - "title": "Copy Stack", - "icon": "$(copy)", - "category": "ZenML" - }, - { - "command": "zenml.goToStackUrl", - "title": "Go to URL", - "icon": "$(globe)", - "category": "ZenML" + "command": "zenml.updateComponent", + "title": "Update Component", + "icon": "$(edit)", + "category": "ZenML Components" }, { "command": "zenml.setPipelineRunsPerPage", @@ -273,12 +285,6 @@ "title": "Restart LSP Server", "icon": "$(debug-restart)", "category": "ZenML Environment" - }, - { - "command": "zenml.createStack", - "title": "Create Stack", - "icon": "$(add)", - "category": "ZenML Stacks" } ], "viewsContainers": { @@ -416,6 +422,11 @@ "command": "zenml.goToStackUrl", "group": "inline@4" }, + { + "when": "componentCommandsRegistered && view == zenmlComponentView && viewItem == stackComponent", + "command": "zenml.updateComponent", + "group": "inline" + }, { "when": "pipelineCommandsRegistered && view == zenmlPipelineView && viewItem == pipelineRun", "command": "zenml.deletePipelineRun", diff --git a/resources/components-form/components.js b/resources/components-form/components.js index 97dca70c..efef985c 100644 --- a/resources/components-form/components.js +++ b/resources/components-form/components.js @@ -8,7 +8,8 @@ let flavor = ''; let id = ''; const inputs = {}; -document.querySelectorAll('.field > input, textarea').forEach(element => { + +document.querySelectorAll('.input').forEach(element => { inputs[element.id] = element; if (element instanceof HTMLTextAreaElement) { element.addEventListener('input', evt => { @@ -29,6 +30,35 @@ document.querySelectorAll('.field > input, textarea').forEach(element => { } }); +const setValues = (name, config) => { + document.querySelector('[name="name"]').value = name; + + console.log(config); + console.log(inputs); + + for (const key in config) { + if (config[key] === null) { + continue; + } + + if (typeof config[key] === 'boolean' && config[key]) { + inputs[config].checked = 'on'; + } + + if (typeof config[key] === 'object') { + inputs[key].value = JSON.stringify(config[key]); + } else { + inputs[key].value = String(config[key]); + } + + if (inputs[key].classList.contains('hidden')) { + inputs[key].classList.toggle('hidden'); + button = document.querySelector(`[data-id="${inputs[key].id}"]`); + button.textContent = '-'; + } + } +}; + form.addEventListener('click', evt => { const target = evt.target; if (!(target instanceof HTMLButtonElement)) { @@ -54,7 +84,7 @@ form.addEventListener('click', evt => { for (const id in inputs) { if (inputs[id].classList.contains('hidden')) { - delete data[id]; + data[id] = null; continue; } @@ -76,6 +106,11 @@ form.addEventListener('click', evt => { submit.disabled = true; spinner.classList.remove('hidden'); + + if (mode === 'update') { + data.id = id; + } + vscode.postMessage({ command: mode, data, @@ -99,7 +134,7 @@ window.addEventListener('message', evt => { type = message.type; flavor = message.flavor; id = message.id; - // TODO: set fields to data values + setValues(message.name, message.config); break; case 'fail': diff --git a/src/commands/components/ComponentsForm.ts b/src/commands/components/ComponentsForm.ts index a5ac424b..77bf0070 100644 --- a/src/commands/components/ComponentsForm.ts +++ b/src/commands/components/ComponentsForm.ts @@ -86,9 +86,39 @@ export default class ComponentForm extends WebviewBase { panel.webview.postMessage({ command: 'create', type: flavor.type, flavor: flavor.name }); } + public async updateForm( + flavor: Flavor, + name: string, + id: string, + config: { [key: string]: any } + ) { + const panel = await this.getPanel(); + const description = flavor.config_schema.description.replaceAll('\n', '
'); + panel.webview.html = this.template({ + type: flavor.type, + flavor: flavor.name, + logo: flavor.logo_url, + description, + docs_url: flavor.docs_url, + sdk_docs_url: flavor.sdk_docs_url, + js: panel.webview.asWebviewUri(this.javaScript), + css: panel.webview.asWebviewUri(this.css), + fields: this.toFormFields(flavor.config_schema), + }); + + panel.webview.postMessage({ + command: 'update', + type: flavor.type, + flavor: flavor.name, + name, + id, + config, + }); + } + private async getPanel(): Promise { const panels = Panels.getInstance(); - const existingPanel = panels.getPanel('component-form'); + const existingPanel = panels.getPanel('component-form', true); if (existingPanel) { existingPanel.reveal(); return existingPanel; @@ -120,7 +150,7 @@ export default class ComponentForm extends WebviewBase { success = await this.createComponent(name, type, flavor, data); break; case 'update': - // TODO + success = await this.updateComponent(id, name, type, data); break; } @@ -168,6 +198,34 @@ export default class ComponentForm extends WebviewBase { return true; } + private async updateComponent( + id: string, + name: string, + type: string, + data: object + ): Promise { + const lsClient = LSClient.getInstance(); + try { + const resp = await lsClient.sendLsClientRequest('updateComponent', [id, type, name, data]); + + if ('error' in resp) { + vscode.window.showErrorMessage(`Unable to update component: "${resp.error}"`); + console.error(resp.error); + traceError(resp.error); + return false; + } + + traceInfo(resp.message); + } catch (e) { + vscode.window.showErrorMessage(`Unable to update component: "${e}"`); + console.error(e); + traceError(e); + return false; + } + + return true; + } + private toFormFields(configSchema: { [key: string]: any }) { const properties = configSchema.properties; const required = configSchema.required ?? []; @@ -280,7 +338,7 @@ export default class ComponentForm extends WebviewBase { id="{{key}}" name="{{key}}" value="{{defaultValue}}" - class="{{#if is_optional}}hidden{{/if}}" + class="input {{#if is_optional}}hidden{{/if}}" {{#if is_required}}required{{/if}} > {{/if}} @@ -290,7 +348,7 @@ export default class ComponentForm extends WebviewBase { type="checkbox" name="{{key}}" id="{{key}}" - class="{{#if is_optional}}hidden{{/if}}" + class="input {{#if is_optional}}hidden{{/if}}" {{#if is_required}}required{{/if}} {{#if defaultValue}}checked{{/if}} > @@ -302,7 +360,7 @@ export default class ComponentForm extends WebviewBase { name="{{key}}" id="{{key}}" value="{{default_value}}" - class="{{#if is_optional}}hidden{{/if}}" + class="input {{#if is_optional}}hidden{{/if}}" {{#if is_required}}required{{/if}} > {{/if}} @@ -311,7 +369,7 @@ export default class ComponentForm extends WebviewBase {