diff --git a/package.json b/package.json index 1ddf0c51..d470ed64 100644 --- a/package.json +++ b/package.json @@ -320,6 +320,52 @@ "command": "microsoft-powerapps-portals.webExtension.init", "title": "%microsoft-powerapps-portals.webExtension.init.title%" }, + { + "command": "powerpages.websitePanel.refresh", + "title": "Refresh", + "icon": "$(refresh)" + }, + { + "command": "powerpages.websitePanel.downloadWebsite", + "title": "Download Website", + "icon": "$(arrow-down)" + }, + { + "command": "powerpages.websitePanel.uploadWebsite", + "title": "Upload Website", + "icon": "$(arrow-up)" + }, + { + "command": "powerpages.websitePanel.uploadWebsiteForceAll", + "title": "Upload Website (force all)" + }, + { + "command": "powerpages.websitePanel.bootstrap-migration", + "title": "Bootstrap Migration" + }, + { + "command": "powerpages.websitePanel.refresh", + "title": "Refresh", + "icon": "$(refresh)" + }, + { + "command": "powerpages.websitePanel.downloadWebsite", + "title": "Download Website", + "icon": "$(arrow-down)" + }, + { + "command": "powerpages.websitePanel.uploadWebsite", + "title": "Upload Website", + "icon": "$(arrow-up)" + }, + { + "command": "powerpages.websitePanel.uploadWebsiteForceAll", + "title": "Upload Website (force all)" + }, + { + "command": "powerpages.websitePanel.bootstrap-migration", + "title": "Bootstrap Migration" + }, { "command": "microsoft-powerapps-portals.webpage", "category": "Powerpages", @@ -672,6 +718,26 @@ "command": "microsoft-powerapps-portals.preview-show", "when": "never" }, + { + "command": "powerpages.websitePanel.refresh", + "when": "never" + }, + { + "command": "powerpages.websitePanel.downloadWebsite", + "when": "never" + }, + { + "command": "powerpages.websitePanel.uploadWebsite", + "when": "never" + }, + { + "command": "powerpages.websitePanel.bootstrap-migration", + "when": "never" + }, + { + "command": "powerpages.websitePanel.uploadWebsiteForceAll", + "when": "never" + }, { "command": "pacCLI.authPanel.refresh", "when": "never" @@ -830,6 +896,11 @@ "command": "powerpages.copilot.clearConversation", "when": "view == powerpages.copilot", "group": "navigation" + }, + { + "command": "powerpages.websitePanel.refresh", + "when": "!virtualWorkspace && view == powerpages.websitePanel", + "group": "navigation@1" } ], "view/item/context": [ @@ -891,6 +962,20 @@ "command": "pacCLI.envAndSolutionsPanel.copyOrganizationId", "when": "!virtualWorkspace && view == pacCLI.envAndSolutionsPanel && viewItem == ENVIRONMENT" }, + { + "command": "powerpages.websitePanel.downloadWebsite", + "when": "!virtualWorkspace && view == powerpages.websitePanel", + "group": "inline" + }, + { + "command": "powerpages.websitePanel.uploadWebsite", + "when": "!virtualWorkspace && view == powerpages.websitePanel", + "group": "inline" + }, + { + "command": "powerpages.websitePanel.bootstrap-migration", + "when": "!virtualWorkspace && view == powerpages.websitePanel" + }, { "command": "powerpages.collaboration.openTeamsChat", "group": "inline", @@ -900,17 +985,57 @@ "command": "powerpages.collaboration.openMail", "group": "inline", "when": "viewItem == userNode" + }, + { + "submenu": "microsoft-powerpages-download" + }, + { + "submenu": "microsoft-powerpages-upload" + } + + ], + "microsoft-powerpages-download":[ + { + "command": "powerpages.websitePanel.downloadWebsite", + "when": "!virtualWorkspace && view == powerpages.websitePanel" + } + ], + "microsoft-powerpages-upload":[ + { + "command": "powerpages.websitePanel.uploadWebsite", + "when": "!virtualWorkspace && view == powerpages.websitePanel" + }, + { + "command": "powerpages.websitePanel.uploadWebsiteForceAll", + "when": "!virtualWorkspace && view == powerpages.websitePanel" } ] + }, "submenus": [ { "id": "microsoft-powerapps-portals.powerpages", "label": "Power Pages" }, + { + "id": "microsoft-powerpages-download", + "label": "Download" + }, + { + "id": "microsoft-powerpages-upload", + "label": "Upload" + }, { "id": "microsoft-powerapps-portals.powerpages-copilot", "label": "Copilot In Power Pages" + }, + { + "id": "microsoft-powerpages-download", + "label": "Download" + }, + { + "id": "microsoft-powerpages-upload", + "label": "Upload" } ], "viewsContainers": { @@ -934,6 +1059,11 @@ "name": "%pacCLI.envAndSolutionsPanel.title%", "when": "!virtualWorkspace && !config.powerPlatform.experimental.disableActivityBarPanels" }, + { + "id": "powerpages.websitePanel", + "name": "Power Pages Websites", + "when": "!virtualWorkspace" + }, { "type": "webview", "id": "powerpages.copilot", diff --git a/src/client/lib/PacActivityBarUI.ts b/src/client/lib/PacActivityBarUI.ts index 7cb9bc09..df8a4649 100644 --- a/src/client/lib/PacActivityBarUI.ts +++ b/src/client/lib/PacActivityBarUI.ts @@ -9,6 +9,7 @@ import { AuthTreeView } from './AuthPanelView'; import { EnvAndSolutionTreeView } from './EnvAndSolutionTreeView'; import { PowerPagesCopilot } from '../../common/copilot/PowerPagesCopilot'; import { ITelemetry } from '../telemetry/ITelemetry'; +import { WebsiteTreeView } from './PowerPagesWebsiteView'; export function RegisterPanels(pacWrapper: PacWrapper, context: vscode.ExtensionContext, telemetry: ITelemetry): vscode.Disposable[] { const authPanel = new AuthTreeView(() => pacWrapper.authList(), pacWrapper); @@ -17,6 +18,8 @@ export function RegisterPanels(pacWrapper: PacWrapper, context: vscode.Extension (environmentUrl) => pacWrapper.solutionListFromEnvironment(environmentUrl), authPanel.onDidChangeTreeData, pacWrapper); + + const websitePanel = new WebsiteTreeView(() => pacWrapper.websiteList(), pacWrapper); const copilotPanel = new PowerPagesCopilot(context.extensionUri, context, telemetry, pacWrapper); @@ -26,5 +29,5 @@ export function RegisterPanels(pacWrapper: PacWrapper, context: vscode.Extension }, }); - return [authPanel, envAndSolutionPanel, copilotPanel]; + return [authPanel, envAndSolutionPanel, copilotPanel, websitePanel]; } diff --git a/src/client/lib/PacTerminal.ts b/src/client/lib/PacTerminal.ts index eb28a7b5..f5746020 100644 --- a/src/client/lib/PacTerminal.ts +++ b/src/client/lib/PacTerminal.ts @@ -46,6 +46,10 @@ export class PacTerminal implements vscode.Disposable { vscode.commands.registerCommand('pacCLI.pacPackageHelp', () => PacTerminal.getTerminal().sendText("pac package help")), vscode.commands.registerCommand('pacCLI.pacPcfHelp', () => PacTerminal.getTerminal().sendText("pac pcf help")), vscode.commands.registerCommand('pacCLI.pacSolutionHelp', () => PacTerminal.getTerminal().sendText("pac solution help"))); + vscode.commands.registerCommand('pacCLI.pacPaportalDownload', (websiteId, path) => PacTerminal.getTerminal().sendText(`pac paportal download -id ${websiteId} -p ${path}`)); + vscode.commands.registerCommand('pacCLI.pacPaportalUpload', (path, modelVersion) => PacTerminal.getTerminal().sendText(`pac paportal upload -p ${path} -mv ${modelVersion}`)) + vscode.commands.registerCommand('pacCLI.pacPaportalUploadForce', (path, modelVersion) => PacTerminal.getTerminal().sendText(`pac paportal upload -p ${path} -mv ${modelVersion} -f`)) + vscode.commands.registerCommand('pacCLI.pacPaportalBootstrapMigration', (path) => PacTerminal.getTerminal().sendText(`pac paportal bootstrap-migrate -p ${path}`)) this._cmdDisposables.push(vscode.commands.registerCommand(`pacCLI.enableTelemetry`, async () => { const result = await this._pacWrapper.enableTelemetry(); diff --git a/src/client/lib/PowerPagesWebsiteView.ts b/src/client/lib/PowerPagesWebsiteView.ts new file mode 100644 index 00000000..5e8afaf7 --- /dev/null +++ b/src/client/lib/PowerPagesWebsiteView.ts @@ -0,0 +1,247 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as os from 'os'; +import path from 'path'; +import * as vscode from 'vscode'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { WebsiteListOutput, WebsiteListing, } from '../pac/PacTypes'; +import { PacWrapper } from '../pac/PacWrapper'; + +export class WebsiteTreeView implements vscode.TreeDataProvider, vscode.Disposable { + private readonly _disposables: vscode.Disposable[] = []; + private _refreshTimeout?: NodeJS.Timeout; + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + constructor( + public readonly dataSource: () => Promise, + pacWrapper: PacWrapper) { + + const watchPath = GetAuthProfileWatchPattern(); + if (watchPath) { + const watcher = vscode.workspace.createFileSystemWatcher(watchPath); + this._disposables.push( + watcher, + watcher.onDidChange(() => this.delayRefresh()), + watcher.onDidCreate(() => this.delayRefresh()), + watcher.onDidDelete(() => this.delayRefresh()) + ); + } + + this._disposables.push(...this.registerPanel(pacWrapper)); + } + + public dispose(): void { + this._disposables.forEach(d => d.dispose()); + } + + // We refresh the Auth Panel by both the FileWatcher events and by direct invocation + // after a executing a create/delete/select/etc command via the UI buttons. + // This can cause doubled refresh (and thus double `pac auth list` and `pac org list` calls). + // We want both routes, but don't want the double refresh, so use a singleton timeout limit + // to a single refresh call. + private delayRefresh(): void { + if (!this._refreshTimeout) { + this._refreshTimeout = setTimeout(() => this.refresh(), 200); + } + } + + refresh(): void { + if (this._refreshTimeout) { + clearTimeout(this._refreshTimeout); + this._refreshTimeout = undefined; + } + this._onDidChangeTreeData.fire(); + } + + public getTreeItem(element: AuthProfileTreeItem): vscode.TreeItem | Thenable { + return element; + } + + public async getChildren(element?: AuthProfileTreeItem): Promise { + if (element) { + // This "Tree" view is a flat list, so return no children when not at the root + return []; + } else { + const pacOutput = await this.dataSource(); + if (pacOutput && pacOutput.Status === "Success" && pacOutput.Information) { + const items = pacOutput.Information + // .filter(item => item.Kind !== "ADMIN") // Only Universal and Dataverse profiles + .map(item => new AuthProfileTreeItem(item)); + return items; + } else { + return []; + } + } + } + + private registerPanel(_: PacWrapper): vscode.Disposable[] { + return [ + vscode.window.registerTreeDataProvider("powerpages.websitePanel", this), + vscode.commands.registerCommand("powerpages.websitePanel.refresh", () => this.refresh()), + vscode.commands.registerCommand("powerpages.websitePanel.downloadWebsite", async () => { + const downloadPath: string | undefined = this.getCurrentWorkspacePath(); + if (downloadPath && downloadPath.length > 0) { + const websiteDownloadPath = this.removeLeadingSlash(downloadPath); + vscode.window.showInformationMessage(vscode.l10n.t("Downloading website...")); + vscode.commands.executeCommand("pacCLI.pacPaportalDownload", '7b9d41f2-9748-ee11-be6f-6045bd072a16' , websiteDownloadPath); + } + }), + vscode.commands.registerCommand("powerpages.websitePanel.uploadWebsite", async () => { + const uploadPath: string | undefined = this.getCurrentWorkspacePath(); + const modelVersion = 1 // get model version from + + if (uploadPath && uploadPath.length > 0) { + const websiteUploadPath = this.removeLeadingSlash(uploadPath); + vscode.window.showInformationMessage(vscode.l10n.t("Uploading website...")); + vscode.commands.executeCommand("pacCLI.pacPaportalUpload", websiteUploadPath, modelVersion); + } + }), + vscode.commands.registerCommand("powerpages.websitePanel.uploadWebsiteForceAll", async () => { + const uploadPath: string | undefined = this.getCurrentWorkspacePath(); + const modelVersion = 1 // get model version from + if (uploadPath && uploadPath.length > 0) { + const websiteUploadPath = this.removeLeadingSlash(uploadPath); + vscode.window.showInformationMessage(vscode.l10n.t("Uploading website (force all)...")); + vscode.commands.executeCommand("pacCLI.pacPaportalUploadForce", websiteUploadPath, modelVersion); + } + }), + vscode.commands.registerCommand("powerpages.websitePanel.bootstrap-migration", async () => { + const uploadPath: string | undefined = this.getCurrentWorkspacePath(); + if (uploadPath && uploadPath.length > 0) { + const websiteUploadPath = this.removeLeadingSlash(uploadPath); + vscode.window.showInformationMessage(vscode.l10n.t("Bootstrap Migration...")); + vscode.commands.executeCommand("pacCLI.pacPaportalBootstrapMigration", websiteUploadPath); + } + }) + // vscode.commands.registerCommand("pacCLI.authPanel.clearAuthProfile", async () => { + // const confirm = vscode.l10n.t("Confirm"); + // const confirmResult = await vscode.window.showWarningMessage( + // vscode.l10n.t("Are you sure you want to clear all the Auth Profiles?"), + // confirm, + // vscode.l10n.t("Cancel")); + // if (confirmResult && confirmResult === confirm) { + // await pacWrapper.authClear(); + // this.delayRefresh(); + // } + // }), + // vscode.commands.registerCommand("pacCLI.authPanel.newAuthProfile", async () => { + // await pacWrapper.authCreateNewAuthProfile(); + // this.delayRefresh(); + // }), + // vscode.commands.registerCommand("pacCLI.authPanel.selectAuthProfile", async (item: AuthProfileTreeItem) => { + // await pacWrapper.authSelectByIndex(item.model.Index); + // this.delayRefresh(); + // }), + // vscode.commands.registerCommand("pacCLI.authPanel.deleteAuthProfile", async (item: AuthProfileTreeItem) => { + // const confirm = vscode.l10n.t("Confirm"); + // const confirmResult = await vscode.window.showWarningMessage( + // vscode.l10n.t({ message: "Are you sure you want to delete the Auth Profile {0}-{1}?", + // args: [item.model.User, item.model.Resource], + // comment: ["{0} is the user name, {1} is the URL of environment of the auth profile"] }), + // confirm, + // vscode.l10n.t("Cancel")); + // if (confirmResult && confirmResult === confirm) { + // await pacWrapper.authDeleteByIndex(item.model.Index); + // this.delayRefresh(); + // } + // }), + // vscode.commands.registerCommand('pacCLI.authPanel.nameAuthProfile', async (item: AuthProfileTreeItem) => { + // const authProfileName = await vscode.window.showInputBox({ + // title: vscode.l10n.t("Name/Rename Auth Profile"), + // prompt: vscode.l10n.t("The name you want to give to this authentication profile"), + // validateInput: value => value.length <= 30 ? null : vscode.l10n.t('Maximum 30 characters allowed') + // }); + // if (authProfileName) { + // await pacWrapper.authNameByIndex(item.model.Index, authProfileName); + // this.delayRefresh(); + // } + // }), + // vscode.commands.registerCommand('pacCLI.authPanel.navigateToResource', (item: AuthProfileTreeItem) => { + // vscode.env.openExternal(vscode.Uri.parse(item.model.Resource)); + // }), + // vscode.commands.registerCommand('pacCLI.authPanel.copyUser', (item: AuthProfileTreeItem) => { + // vscode.env.clipboard.writeText(item.model.User); + // }) + ]; + } + + private removeLeadingSlash(path: string): string { + if (path.startsWith("/")) { + return path.slice(1); + } + return path; + } + + private getCurrentWorkspacePath(): string | undefined { + if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { + return vscode.workspace.workspaceFolders[0].uri.fsPath; + } + return undefined; + } +} + +class AuthProfileTreeItem extends vscode.TreeItem { + public constructor(public readonly model: string) { + super(AuthProfileTreeItem.createLabel(model), vscode.TreeItemCollapsibleState.Collapsed); + this.contextValue = model; + //this.tooltip = AuthProfileTreeItem.createTooltip(model); + // if (model.IsActive){ + // this.iconPath = new vscode.ThemeIcon("star-full") + // } + } + private static createLabel(profile: string): string { + // if (profile.Name) { + // return `${profile.Kind}: ${profile.Name}`; + // } else if (profile.Kind === "ADMIN" || profile.Kind === "UNIVERSAL") { + // return `${profile.Kind}: ${profile.User}`; + // } else { + // return `${profile.Kind}: ${profile.Resource}`; + // } + return `${profile}`; + } + // private static createTooltip(profile: WebsiteListing): string { + // const tooltip = [ + // vscode.l10n.t({ + // message: "Profile Kind: {0}", + // args: [profile.Kind], + // comment: ["The {0} represents the profile type (Admin vs Dataverse)"]}) + // ]; + // if (profile.Name) { + // tooltip.push(vscode.l10n.t({ + // message: "Name: {0}", + // args: [profile.Name], + // comment: ["The {0} represents the optional name the user provided for the profile)"]})); + // } + + // tooltip.push(vscode.l10n.t({ + // message: "User: {0}", + // args: [profile.User], + // comment: ["The {0} represents auth profile's user name (email address))"]})); + // if (profile.CloudInstance) { + // tooltip.push(vscode.l10n.t({ + // message: "Cloud Instance: {0}", + // args: [profile.CloudInstance], + // comment: ["The {0} represents profile's Azure Cloud Instances"]})); + // } + // return tooltip.join('\n'); + // } +} + +export function GetAuthProfileWatchPattern(): vscode.RelativePattern | undefined { + if (os.platform() === 'win32') { + return process.env.LOCALAPPDATA + ? new vscode.RelativePattern(path.join(process.env.LOCALAPPDATA, "Microsoft", "PowerAppsCli"), "authprofiles*.json") + : undefined; + } + else if (os.platform() === 'linux' || os.platform() === 'darwin') { + return process.env.HOME + ? new vscode.RelativePattern(path.join(process.env.HOME, ".local", "share", "Microsoft", "PowerAppsCli"), "authprofiles*.json") + : undefined + } + + return undefined; +} diff --git a/src/client/pac/PacTypes.ts b/src/client/pac/PacTypes.ts index b4960914..f8dde7ed 100644 --- a/src/client/pac/PacTypes.ts +++ b/src/client/pac/PacTypes.ts @@ -72,4 +72,17 @@ export type ActiveOrgOutput = { EnvironmentId: string, } +export type WebsiteListOutput = PacOutput & { + Results: WebsiteListing[]; +} + +export type WebsiteListing = { + Index: number; + WebsiteId: string; + WebsiteName: string; +} + + export type PacOrgWhoOutput = PacOutputWithResult; + + diff --git a/src/client/pac/PacWrapper.ts b/src/client/pac/PacWrapper.ts index 0300c6b5..28331fd1 100644 --- a/src/client/pac/PacWrapper.ts +++ b/src/client/pac/PacWrapper.ts @@ -10,7 +10,7 @@ import * as fs from "fs-extra"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import { BlockingQueue } from "../../common/utilities/BlockingQueue"; import { ITelemetry } from "../telemetry/ITelemetry"; -import { PacOutput, PacAdminListOutput, PacAuthListOutput, PacSolutionListOutput, PacOrgListOutput, PacOrgWhoOutput } from "./PacTypes"; +import { PacOutput, PacAdminListOutput, PacAuthListOutput, PacSolutionListOutput, PacOrgListOutput, PacOrgWhoOutput, WebsiteListOutput } from "./PacTypes"; import { v4 } from "uuid"; import { oneDSLoggerWrapper } from "../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; @@ -180,6 +180,10 @@ export class PacWrapper { return this.executeCommandAndParseResults(new PacArguments("telemetry", "disable")); } + public async websiteList(): Promise { + return this.executeCommandAndParseResults(new PacArguments("paportal", "list")); + } + public exit() : void { this.pacInterop.exit(); }