diff --git a/package-lock.json b/package-lock.json index 71b63e2..ad8b5d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "conditional-reduce": "^1.2.0", "dayjs": "^1.10.7", "dts-dom": "^3.6.0", + "dvdt-erd-generator": "^0.0.6", "fast-xml-parser": "^4.5.0", "fs-extra": "^10.0.0", "lodash": "^4.17.21", @@ -2641,6 +2642,20 @@ "node": ">= 0.4" } }, + "node_modules/dvdt-erd-generator": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/dvdt-erd-generator/-/dvdt-erd-generator-0.0.6.tgz", + "integrity": "sha512-qQVSMPAU9sUzlhHYCyAmccxe7rBBcRAFH2DX1f+NdI6rxS2A9OKdhByRth3vLaVMOHuNGYblgKOk6lCBph3b9w==", + "license": "GPL-2.0", + "peerDependencies": { + "vscode": "^1.85.0" + }, + "peerDependenciesMeta": { + "vscode": { + "optional": true + } + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", diff --git a/package.json b/package.json index 278d0a4..a3f8b75 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,12 @@ "when": "view == dvEntities", "group": "navigation@2" }, + { + "command": "dvdt.commands.showToolsPage", + "when": "view == ppToolBox", + "group": "navigation@1", + "icon": "$(tools)" + }, { "command": "dvdt.explorer.webresources.smartMatch", "when": "view == dvWebResources", @@ -506,6 +512,18 @@ "title": "Open Dataverse REST Builder (DRB)", "category": "%dvdt.commands.category%" }, + { + "command": "dvdt.commands.showToolsPage", + "title": "Show Tools Page", + "category": "%dvdt.commands.category%", + "icon": "$(tools)" + }, + { + "command": "dvdt.commands.launchToolByShortName", + "title": "Launch Tool", + "when": "false", + "category": "%dvdt.commands.category%" + }, { "command": "dvdt.explorer.tools.launchTool", "title": "Launch tool from ToolBox", @@ -579,6 +597,7 @@ "conditional-reduce": "^1.2.0", "dayjs": "^1.10.7", "dts-dom": "^3.6.0", + "dvdt-erd-generator": "^0.0.6", "fast-xml-parser": "^4.5.0", "fs-extra": "^10.0.0", "lodash": "^4.17.21", diff --git a/resources/views/css/base.css b/resources/views/css/base.css index fe1a21c..87206b4 100644 --- a/resources/views/css/base.css +++ b/resources/views/css/base.css @@ -69,6 +69,191 @@ body { font-size: small; } +.btn { + padding: 4px 12px; + cursor: pointer; + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 2px; + font-size: 0.8rem; +} + +.btn:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.btn:active { + background-color: var(--vscode-button-hoverBackground); + opacity: 0.8; +} + +/**** Tools Page Styling ****/ +.tools-header { + padding: 20px; + border-bottom: 1px solid var(--vscode-panel-border); + margin-bottom: 20px; +} + +.tools-header h2 { + margin: 0 0 8px 0; + color: var(--vscode-foreground); + font-size: 1.5rem; + font-weight: 600; +} + +.tools-description { + margin: 0 0 16px 0; + color: var(--vscode-descriptionForeground); + font-size: 0.9rem; +} + +.search-container { + width: 100%; + max-width: 600px; +} + +.tools-search { + width: 100%; + padding: 10px 16px; + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border); + border-radius: 4px; + font-size: 0.9rem; + outline: none; + transition: border-color 0.2s; +} + +.tools-search:focus { + border-color: var(--vscode-focusBorder); +} + +.tools-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 16px; + padding: 0 20px 20px 20px; +} + +.tool-card { + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + padding: 20px; + transition: all 0.2s ease; + cursor: pointer; + display: flex; + flex-direction: column; + gap: 12px; +} + +.tool-card:hover { + border-color: var(--vscode-focusBorder); + background-color: var(--vscode-list-hoverBackground); + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.tool-card-header { + display: flex; + align-items: center; + gap: 12px; +} + +.tool-icon { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, var(--vscode-button-background), var(--vscode-button-hoverBackground)); + border-radius: 8px; + font-size: 24px; + flex-shrink: 0; +} + +.tool-info { + flex: 1; + min-width: 0; +} + +.tool-name { + font-size: 1.1rem; + font-weight: 600; + color: var(--vscode-foreground); + margin: 0 0 4px 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tool-author { + font-size: 0.85rem; + color: var(--vscode-descriptionForeground); + margin: 0; +} + +.tool-description { + font-size: 0.9rem; + color: var(--vscode-foreground); + line-height: 1.4; + margin: 0; + height: 64px; + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 3; + line-clamp: 3; + display: -webkit-box; + -webkit-box-orient: vertical; +} + +.tool-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 8px; +} + +.tool-tag { + display: inline-block; + padding: 4px 10px; + background-color: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + border-radius: 12px; + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; +} + +.tool-launch-btn { + padding: 8px 16px; + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 4px; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 6px; +} + +.tool-launch-btn:hover { + background-color: var(--vscode-button-hoverBackground); + transform: scale(1.05); +} + +.tool-launch-btn:active { + transform: scale(0.98); +} + +.tool-card.hidden { + display: none; +} + /**** Lables ****/ label { font-size: 0.8rem; diff --git a/resources/views/js/base.js b/resources/views/js/base.js index fb11c16..cef3f29 100644 --- a/resources/views/js/base.js +++ b/resources/views/js/base.js @@ -17,6 +17,14 @@ $(document).ready(function () { }); }); + $("#toolsSearch").on("keyup", function () { + var value = $(this).val().toLowerCase(); + $(".tool-card").filter(function () { + var isMatch = $(this).text().toLowerCase().indexOf(value) > -1; + $(this).toggleClass("hidden", !isMatch); + }); + }); + if ($matchTable && $matchTable.bootstrapTable) { $matchTable.bootstrapTable(); $matchTable.bootstrapTable("refreshOptions", { @@ -80,6 +88,13 @@ function link(fullPath, wrId) { }); } +function launchTool(toolShortName) { + vscode.postMessage({ + command: "launchTool", + toolShortName: toolShortName, + }); +} + function sortTable(tableId, column) { var table = document.getElementById(tableId); var tbody = table.querySelector("tbody"); diff --git a/resources/views/toolslist.html b/resources/views/toolslist.html new file mode 100644 index 0000000..8ee90b7 --- /dev/null +++ b/resources/views/toolslist.html @@ -0,0 +1,11 @@ +
+

Power Platform ToolBox

+

Explore and launch powerful tools for Dataverse development

+
+ +
+
+ +
+ !!{tools} +
diff --git a/src/commands/registerToolsCommands.ts b/src/commands/registerToolsCommands.ts index 30b425a..fc38124 100644 --- a/src/commands/registerToolsCommands.ts +++ b/src/commands/registerToolsCommands.ts @@ -4,6 +4,7 @@ import { CLIHelper } from "../helpers/cliHelper"; import { ErrorHandler } from "../helpers/errorHandler"; import { ToolsHelper } from "../helpers/toolsHelper"; import { ICommand } from "../utils/Interfaces"; +import { ToolsListView } from "../views/ToolsListView"; import { ViewBase } from "../views/ViewBase"; export async function registerToolsCommands(vscontext: vscode.ExtensionContext, tr: TelemetryReporter): Promise { @@ -23,16 +24,16 @@ export async function registerToolsCommands(vscontext: vscode.ExtensionContext, } }, }, - // { - // command: "dvdt.commands.openERDGenerator", - // callback: async () => { - // try { - // toolHelper.openERDGenerator(); - // } catch (error) { - // errorHandler.log(error, "openERDGenerator"); - // } - // }, - // }, + { + command: "dvdt.commands.openERDGenerator", + callback: async () => { + try { + toolHelper.openERDGenerator(); + } catch (error) { + errorHandler.log(error, "openERDGenerator"); + } + }, + }, { command: "dvdt.commands.launchPRT", callback: async () => { @@ -63,6 +64,27 @@ export async function registerToolsCommands(vscontext: vscode.ExtensionContext, } }, }, + { + command: "dvdt.commands.showToolsPage", + callback: async () => { + try { + const webview = await views.getWebView({ type: "showToolsList", title: "Power Platform ToolBox" }); + new ToolsListView(webview, vscontext); + } catch (error) { + errorHandler.log(error, "showToolsPage"); + } + }, + }, + { + command: "dvdt.commands.launchToolByShortName", + callback: async (toolShortName: string) => { + try { + toolHelper.launchToolByShortName(toolShortName, cliHelper, views); + } catch (error) { + errorHandler.log(error, "launchToolByShortName"); + } + }, + }, ); cmds.forEach((c) => { diff --git a/src/helpers/toolsHelper.ts b/src/helpers/toolsHelper.ts index 2853281..8a7abb0 100644 --- a/src/helpers/toolsHelper.ts +++ b/src/helpers/toolsHelper.ts @@ -1,3 +1,4 @@ +import { showERDPanel } from "dvdt-erd-generator"; import * as vscode from "vscode"; import { ToolsTreeItem } from "../tools/toolsDataProvider"; import { connectionCurrentStoreKey } from "../utils/Constants"; @@ -21,14 +22,17 @@ export class ToolsHelper { public openTool(toolItem: ToolsTreeItem) { const views = new ViewBase(this.vscontext); const cliHelper = new CLIHelper(this.vscontext); + this.launchToolByShortName(toolItem.toolShortName, cliHelper, views); + } - switch (toolItem.toolShortName) { + public launchToolByShortName(shortName: string, cliHelper: CLIHelper, views: ViewBase) { + switch (shortName) { case "drb": this.openDRB(views); break; - // case "erd": - // this.openERDGenerator(); - // break; + case "erd": + this.openERDGenerator(); + break; case "prt": cliHelper.launchPRT(); break; @@ -53,12 +57,12 @@ export class ToolsHelper { } } - // public openERDGenerator(): void { - // const connFromWS: IConnection = this.vsstate.getFromWorkspace(connectionCurrentStoreKey); - // if (connFromWS && connFromWS.currentAccessToken) { - // showERDPanel(this.vscontext.extensionUri, connFromWS.environmentUrl, connFromWS.currentAccessToken); - // } else { - // vscode.window.showErrorMessage(ErrorMessages.commonToolsError); - // } - // } + public openERDGenerator(): void { + const connFromWS: IConnection = this.vsstate.getFromWorkspace(connectionCurrentStoreKey); + if (connFromWS && connFromWS.currentAccessToken) { + showERDPanel(this.vscontext.extensionUri, connFromWS.environmentUrl, connFromWS.currentAccessToken); + } else { + vscode.window.showErrorMessage(ErrorMessages.commonToolsError); + } + } } diff --git a/src/tools/tools.json b/src/tools/tools.json index af36cb3..65bbf21 100644 --- a/src/tools/tools.json +++ b/src/tools/tools.json @@ -4,25 +4,36 @@ "name": "Dataverse REST Builder", "shortName": "drb", "author": "Guido Preite", - "icon": "drb.png" + "icon": "drb.png", + "description": "Build and test Dataverse Web API queries with an intuitive interface. Generate code snippets for JavaScript, C#, and more." + }, + { + "name": "ERD Generator", + "shortName": "erd", + "author": "Power Maverick", + "icon": "erd.svg", + "description": "Generate Entity Relationship Diagrams for Dataverse models." }, { "name": "Plugin Registration", "shortName": "prt", "author": "Microsoft", - "icon": "prt.png" + "icon": "prt.png", + "description": "Register and manage plugins, custom workflow activities, and service endpoints for Dataverse." }, { "name": "Configuration Migration", "shortName": "cmt", "author": "Microsoft", - "icon": "cmt.png" + "icon": "cmt.png", + "description": "Move configuration data across Dataverse environments. Export and import customizations, reference data, and more." }, { "name": "Package Deployer", "shortName": "pd", "author": "Microsoft", - "icon": "pd.png" + "icon": "pd.png", + "description": "Deploy packages to Dataverse environments with pre and post-deployment actions. Automate solution deployment." } ] } \ No newline at end of file diff --git a/src/utils/Interfaces.ts b/src/utils/Interfaces.ts index 3e5debc..d6272ba 100644 --- a/src/utils/Interfaces.ts +++ b/src/utils/Interfaces.ts @@ -298,6 +298,7 @@ export interface IToolDetails { shortName: string; author: string; icon?: string; + description?: string; } export interface ICliCommandList { commands: ICliCommand[]; diff --git a/src/views/ToolsListView.ts b/src/views/ToolsListView.ts new file mode 100644 index 0000000..7152f00 --- /dev/null +++ b/src/views/ToolsListView.ts @@ -0,0 +1,68 @@ +import _ from "lodash"; +import * as path from "path"; +import * as vscode from "vscode"; +import toolsInJson from "../tools/tools.json"; +import { readFileSync } from "../utils/FileSystem"; +import { IToolDetails } from "../utils/Interfaces"; +import { VsCodePanel } from "./base/VsCodePanelBase"; + +export class ToolsListView extends VsCodePanel { + private tools: IToolDetails[]; + + constructor(webview: vscode.WebviewPanel, vscontext: vscode.ExtensionContext) { + super({ panel: webview, extensionUri: vscontext.extensionUri, webViewFileName: "toolslist.html" }); + this.tools = toolsInJson.tools; + // Set the webview's initial html content + super.update(); + + // Set up message listener for launching tools + this.webViewPanel.webview.onDidReceiveMessage((message) => { + switch (message.command) { + case "launchTool": + vscode.commands.executeCommand("dvdt.commands.launchToolByShortName", message.toolShortName); + return; + } + }); + } + + public getHtmlForWebview(webviewFileName: string): string { + const pathOnDisk = path.join(this.panelOptions.extensionUri.fsPath, "resources", "views", webviewFileName); + const fileHtml = readFileSync(pathOnDisk).toString(); + _.templateSettings.interpolate = /!!{([\s\S]+?)}/g; + const compiled = _.template(fileHtml); + + const viewModel = { + tools: this.tools + .map((tool) => { + const escapedToolName = _.escape(tool.name); + const escapedShortName = _.escape(tool.shortName); + const escapedAuthor = _.escape(tool.author); + const escapedDescription = _.escape(tool.description || "No description available"); + const toolIcon = tool.icon + ? `${escapedToolName} Icon` + : `🛠️`; + + return `
+
+
${toolIcon}
+
+

${escapedToolName}

+

by ${escapedAuthor}

+
+
+

${escapedDescription}

+ +
`; + }) + .join(""), + }; + + return super.render(compiled(viewModel)); + } +}