diff --git a/package.json b/package.json index d6fcd7c6b..9de7b351f 100644 --- a/package.json +++ b/package.json @@ -170,6 +170,11 @@ "view": "microsoft.powerplatform.pages.actionsHub", "contents": "%microsoft.powerplatform.pages.actionsHub.login%", "when": "!virtualWorkspace && pacCLI.authPanel.interactiveLoginSupported" + }, + { + "view": "microsoft.powerplatform.pages.metadataDiff", + "contents": "%microsoft.powerplatform.pages.metadataDiff.login%", + "when": "!virtualWorkspace && pacCLI.authPanel.interactiveLoginSupported" } ], "commands": [ @@ -439,6 +444,36 @@ { "command": "microsoft.powerplatform.pages.actionsHub.activeSite.uploadSite", "title": "%microsoft.powerplatform.pages.actionsHub.activeSite.uploadSite.title%" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.triggerFlow", + "title": "Connect & Download", + "category": "Power Pages Metadata Diff", + "icon": "$(cloud-download)" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.generateReport", + "title": "Generate Report", + "category": "Power Pages Metadata Diff", + "icon": "$(markdown)" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.clearView", + "title": "Clear View", + "category": "Power Pages Metadata Diff", + "icon": "$(clear-all)" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.exportReport", + "title": "Export Report", + "category": "Power Pages Metadata Diff", + "icon": "$(save-as)" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.importReport", + "title": "Import Report", + "category": "Power Pages Metadata Diff", + "icon": "$(folder-opened)" } ], "configuration": { @@ -950,6 +985,31 @@ "command": "powerpages.copilot.clearConversation", "when": "view == powerpages.copilot", "group": "navigation" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.generateReport", + "when": "view == microsoft.powerplatform.pages.metadataDiff", + "group": "navigation" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.triggerFlow", + "when": "view == microsoft.powerplatform.pages.metadataDiff", + "group": "navigation" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.clearView", + "when": "view == microsoft.powerplatform.pages.metadataDiff", + "group": "navigation" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.exportReport", + "when": "view == microsoft.powerplatform.pages.metadataDiff", + "group": "navigation" + }, + { + "command": "microsoft.powerplatform.pages.metadataDiff.importReport", + "when": "view == microsoft.powerplatform.pages.metadataDiff", + "group": "navigation" } ], "view/item/context": [ @@ -1141,6 +1201,14 @@ "icon": "./src/client/assets/powerPages.svg", "contextualTitle": "%microsoft.powerplatform.pages.actionsHub.title%", "visibility": "visible" + }, + { + "id": "microsoft.powerplatform.pages.metadataDiff", + "name": "%microsoft.powerplatform.pages.metadataDiff.title%", + "when": "microsoft.powerplatform.pages.metadataDiffEnabled", + "icon": "./src/client/assets/powerPages.svg", + "contextualTitle": "%microsoft.powerplatform.pages.metadataDiff.title%", + "visibility": "visible" } ] }, diff --git a/package.nls.json b/package.nls.json index 89a2ac4b3..b2e24d7db 100644 --- a/package.nls.json +++ b/package.nls.json @@ -110,5 +110,13 @@ "microsoft.powerplatform.pages.actionsHub.currentActiveSite.revealInOS.mac.title": "Reveal in Finder", "microsoft.powerplatform.pages.actionsHub.currentActiveSite.revealInOS.linux.title": "Open Containing Folder", "microsoft.powerplatform.pages.actionsHub.inactiveSite.openSiteManagement.title": "Open site management", - "microsoft.powerplatform.pages.actionsHub.activeSite.uploadSite.title": "Upload Site" + "microsoft.powerplatform.pages.actionsHub.activeSite.uploadSite.title": "Upload Site", + "microsoft.powerplatform.pages.metadataDiff.title": "POWER PAGES METADATA COMPARATOR", + "microsoft.powerplatform.pages.metadataDiff.login":{ + "message": "Compare your Power Pages website against a Power Pages environment to view any differences. [Learn more](https://go.microsoft.com/fwlink/?linkid=2305702).\n[Get Started](command:microsoft.powerplatform.pages.metadataDiff.triggerFlow)", + "comment": [ + "This is a Markdown formatted string, and the formatting must persist across translations.", + "The second line should be '[TRANSLATION HERE](command:microsoft.powerplatform.pages.metadataDiff.triggerFlow).', keeping brackets and the text in the parentheses unmodified" + ] + } } diff --git a/src/client/extension.ts b/src/client/extension.ts index 56f18103a..ff0ef7699 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -47,6 +47,7 @@ import { ActionsHub } from "./power-pages/actions-hub/ActionsHub"; import { extractAuthInfo, extractOrgInfo } from "./power-pages/commonUtility"; import PacContext from "./pac/PacContext"; import ArtemisContext from "./ArtemisContext"; +import { MetadataDiffDesktop } from "./power-pages/metadata-diff/MetadataDiffDesktop"; let client: LanguageClient; let _context: vscode.ExtensionContext; @@ -260,6 +261,8 @@ export async function activate( const workspaceFolderWatcher = vscode.workspace.onDidChangeWorkspaceFolders(handleWorkspaceFolderChange); _context.subscriptions.push(workspaceFolderWatcher); + MetadataDiffDesktop.initialize(context, pacTerminal) + if (shouldEnableDebugger()) { activateDebugger(context); } diff --git a/src/client/pac/PacTypes.ts b/src/client/pac/PacTypes.ts index de37b0f63..e43b55874 100644 --- a/src/client/pac/PacTypes.ts +++ b/src/client/pac/PacTypes.ts @@ -62,6 +62,14 @@ export type OrgListOutput = { export type PacOrgListOutput = PacOutputWithResultList; +export type PagesList = { + FriendlyName: string, + WebsiteId: string, + ModelVersion: string +} + +export type PacPagesListOutput = PacOutputWithResultList; + export type ActiveOrgOutput = { OrgId: string, UniqueName: string, diff --git a/src/client/pac/PacWrapper.ts b/src/client/pac/PacWrapper.ts index d8803e2ac..be0647b1e 100644 --- a/src/client/pac/PacWrapper.ts +++ b/src/client/pac/PacWrapper.ts @@ -9,7 +9,7 @@ import * as readline from "readline"; import * as fs from "fs-extra"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import { BlockingQueue } from "../../common/utilities/BlockingQueue"; -import { PacOutput, PacAdminListOutput, PacAuthListOutput, PacSolutionListOutput, PacOrgListOutput, PacOrgWhoOutput, PacAuthWhoOutput } from "./PacTypes"; +import { PacOutput, PacAdminListOutput, PacAuthListOutput, PacSolutionListOutput, PacOrgListOutput, PacOrgWhoOutput, PacAuthWhoOutput, PacPagesListOutput} from "./PacTypes"; import { v4 } from "uuid"; import { oneDSLoggerWrapper } from "../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; @@ -180,6 +180,14 @@ export class PacWrapper { return this.executeCommandAndParseResults(new PacArguments("telemetry", "disable")); } + public async pagesDownload(path: string, websiteId: string, modelVersion: string): Promise { + return this.pacInterop.executeCommand(new PacArguments("pages", "download", "--path", path, "--webSiteId", websiteId, "--modelVersion", modelVersion)); + } + + public async pagesList(): Promise { + return this.executeCommandAndParseResults(new PacArguments("pages", "list", "--verbose")); + } + public exit(): void { this.pacInterop.exit(); } diff --git a/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts b/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts new file mode 100644 index 000000000..d73d31895 --- /dev/null +++ b/src/client/power-pages/metadata-diff/MetadataDiffCommands.ts @@ -0,0 +1,346 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as path from "path"; +import { PacTerminal } from "../../lib/PacTerminal"; +import { Constants, SUCCESS } from "../../../common/power-pages/metadata-diff/Constants"; +import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; +import { MetadataDiffTreeDataProvider } from "../../../common/power-pages/metadata-diff/MetadataDiffTreeDataProvider"; +import { createAuthProfileExp } from "../../../common/utilities/PacAuthUtil"; +import { getWebsiteRecordId } from "../../../common/utilities/WorkspaceInfoFinderUtil"; +import { MetadataDiffDesktop } from "./MetadataDiffDesktop"; +import { generateDiffReport, getAllDiffFiles, MetadataDiffReport } from "./MetadataDiffUtils"; + +export async function registerMetadataDiffCommands(context: vscode.ExtensionContext, pacTerminal: PacTerminal): Promise { + // Register command for handling file diffs + vscode.commands.registerCommand('metadataDiff.openDiff', async (workspaceFile: string, storedFile: string) => { + try { + const workspaceUri = vscode.Uri.file(workspaceFile); + const storedUri = vscode.Uri.file(storedFile); + const fileName = path.basename(workspaceFile); + + await vscode.commands.executeCommand('vscode.diff', + storedUri, + workspaceUri, + `${fileName} (Metadata Diff)` + ); + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_REPORT_FAILED, + error as string, + error as Error + ); + vscode.window.showErrorMessage("Failed to open diff view"); + } + }); + + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.clearView", async () => { + try { + MetadataDiffDesktop.resetTreeView(); + + // Set the context variable to false to show welcome message + await vscode.commands.executeCommand("setContext", "microsoft.powerplatform.pages.metadataDiff.hasData", false); + + // Reload tree data provider to show welcome message + const treeDataProvider = MetadataDiffTreeDataProvider.initialize(context); + context.subscriptions.push( + vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) + ); + + vscode.window.showInformationMessage("Metadata diff view cleared successfully."); + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_REPORT_FAILED, + error as string, + error as Error + ); + vscode.window.showErrorMessage("Failed to clear metadata diff view"); + } + }); + + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.triggerFlow", async () => { + try { + // Get the PAC wrapper to access org list + const pacWrapper = pacTerminal.getWrapper(); + + let orgUrl: string | undefined; + + // Get list of available organizations + const orgListResult = await pacWrapper.orgList(); + + if (orgListResult && orgListResult.Status === SUCCESS && orgListResult.Results.length > 0) { + // Create items for QuickPick + const items = orgListResult.Results.map(org => { + return { + label: org.FriendlyName, + description: org.EnvironmentUrl, + detail: `${org.OrganizationId} (${org.EnvironmentId})` + }; + }); + + // Add option to enter URL manually + items.push({ + label: "$(plus) Enter organization URL manually", + description: "", + detail: "Enter a custom organization URL" + }); + + // Show QuickPick to select environment + const selected = await vscode.window.showQuickPick(items, { + placeHolder: "Select an environment or enter URL manually", + ignoreFocusOut: true + }); + + if (selected) { + if (selected.description) { + // Use the selected org URL + orgUrl = selected.description; + } else { + // If manual entry option was selected + orgUrl = await vscode.window.showInputBox({ + prompt: "Enter the organization URL", + placeHolder: "https://your-org.crm.dynamics.com", + validateInput: (input) => { + const urlPattern = /^https:\/\/[a-zA-Z0-9.-]+\d*\.crm\.dynamics\.com\/?$/; + return urlPattern.test(input) ? null : "Please enter a valid URL in the format: https://your-org.crm.dynamics.com"; + } + }); + } + } + } else { + // Fallback to manual entry if no orgs are found + orgUrl = await vscode.window.showInputBox({ + prompt: "Enter the organization URL", + placeHolder: "https://your-org.crm.dynamics.com" + }); + } + + if (!orgUrl) { + vscode.window.showErrorMessage("Organization URL is required to trigger the metadata diff flow."); + return; + } + + const urlPattern = /^https:\/\/[a-zA-Z0-9.-]+\d*\.crm\.dynamics\.com\/?$/; + if (!urlPattern.test(orgUrl)) { + vscode.window.showErrorMessage("Invalid organization URL. Please enter a valid URL in the format: https://your-org.crm.dynamics.com", { modal: true }); + return; + } + + const pacActiveOrg = await pacWrapper.activeOrg(); + if(pacActiveOrg){ + if (pacActiveOrg.Status === SUCCESS) { + if(pacActiveOrg.Results.OrgUrl == orgUrl){ + vscode.window.showInformationMessage("Already connected to the specified environment."); + } + else{ + const pacOrgSelect = await pacWrapper.orgSelect(orgUrl); + if(pacOrgSelect && pacOrgSelect.Status === SUCCESS){ + vscode.window.showInformationMessage("Environment switched successfully."); + } + else{ + vscode.window.showErrorMessage("Failed to switch the environment."); + return; + } + } + } + else{ + await createAuthProfileExp(pacWrapper, orgUrl); + vscode.window.showInformationMessage("Auth profile created successfully."); + } + } + else { + vscode.window.showErrorMessage("Failed to fetch the current environment details."); + return; + } + + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + vscode.window.showErrorMessage("No folders opened in the current workspace."); + return; + } + + const currentWorkspaceFolder = workspaceFolders[0].uri.fsPath; + const websiteId = getWebsiteRecordId(currentWorkspaceFolder); + path.join(websiteId, "metadataDiffStorage"); + const storagePath = context.storageUri?.fsPath; + if (!storagePath) { + throw new Error("Storage path is not defined"); + } + + // Clean out any existing files in storagePath (so we have a "fresh" download) + if (fs.existsSync(storagePath)) { + fs.rmSync(storagePath, { recursive: true, force: true }); + } + fs.mkdirSync(storagePath, { recursive: true }); + const progressOptions: vscode.ProgressOptions = { + location: vscode.ProgressLocation.Notification, + title: "Downloading website metadata", + cancellable: false + }; + let pacPagesDownload; + await vscode.window.withProgress(progressOptions, async (progress) => { + progress.report({ message: "Looking for this website in the connected environment..." }); + const pacPagesList = await MetadataDiffDesktop.getPagesList(pacTerminal); + if (pacPagesList && pacPagesList.length > 0) { + const websiteRecord = pacPagesList.find((record) => record.id === websiteId); + if (!websiteRecord) { + vscode.window.showErrorMessage("Website not found in the connected environment."); + return; + } + progress.report({ message: `Downloading "${websiteRecord.name}" as ${websiteRecord.modelVersion === "v2" ? "enhanced" : "standard"} data model. Please wait...` }); + pacPagesDownload = await pacWrapper.pagesDownload(storagePath, websiteId, websiteRecord.modelVersion == "v1" ? "1" : "2"); + vscode.window.showInformationMessage("Download completed."); + } + }); + if (pacPagesDownload) { + const treeDataProvider = MetadataDiffTreeDataProvider.initialize(context); + context.subscriptions.push( + vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) + ); + } + else{ + vscode.window.showErrorMessage("Failed to download metadata."); + } + + } + catch (error) { + oneDSLoggerWrapper.getLogger().traceError(Constants.EventNames.METADATA_DIFF_REFRESH_FAILED, error as string, error as Error, { methodName: null }, {}); + } + }); + + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.generateReport", async () => { + try { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + vscode.window.showErrorMessage("No workspace folder open"); + return; + } + + const storagePath = context.storageUri?.fsPath; + if (!storagePath) { + vscode.window.showErrorMessage("Storage path not found"); + return; + } + + // Generate report content + const reportContent = await generateDiffReport(workspaceFolders[0].uri.fsPath, storagePath); + + // Create the markdown document + const doc = await vscode.workspace.openTextDocument({ + content: reportContent, + language: 'markdown' + }); + + // Show the document in column One + await vscode.window.showTextDocument(doc, { + preview: false, // Don't use preview mode to ensure stability + viewColumn: vscode.ViewColumn.One, + preserveFocus: false // Ensure focus is on the document + }); + + // Increase delay to ensure document is fully loaded and stable + await new Promise(resolve => setTimeout(resolve, 800)); + + // Close any existing preview first to avoid conflicts + await vscode.commands.executeCommand('markdown.preview.refresh'); + + // Show preview in side-by-side mode + await vscode.commands.executeCommand('markdown.showPreviewToSide'); + + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_REPORT_FAILED, + error as string, + error as Error + ); + vscode.window.showErrorMessage("Failed to generate metadata diff report"); + } + }); + + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.exportReport", async () => { + try { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + vscode.window.showErrorMessage("No workspace folder open"); + return; + } + + const storagePath = context.storageUri?.fsPath; + if (!storagePath) { + vscode.window.showErrorMessage("Storage path not found"); + return; + } + + // Get diff files + const diffFiles = await getAllDiffFiles(workspaceFolders[0].uri.fsPath, storagePath); + + // Create report object + const report: MetadataDiffReport = { + generatedOn: new Date().toISOString(), + files: diffFiles + }; + + // Save dialog + const saveUri = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file('metadata-diff-report.json'), + filters: { + 'JSON files': ['json'] + } + }); + + if (saveUri) { + fs.writeFileSync(saveUri.fsPath, JSON.stringify(report, null, 2)); + vscode.window.showInformationMessage("Report exported successfully"); + } + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_REPORT_FAILED, + error as string, + error as Error + ); + vscode.window.showErrorMessage("Failed to export metadata diff report"); + } + }); + + vscode.commands.registerCommand("microsoft.powerplatform.pages.metadataDiff.importReport", async () => { + try { + const fileUri = await vscode.window.showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + filters: { + 'JSON files': ['json'] + } + }); + + if (fileUri && fileUri[0]) { + const reportContent = fs.readFileSync(fileUri[0].fsPath, 'utf8'); + const report = JSON.parse(reportContent) as MetadataDiffReport; + + // Clean up any existing tree data provider + const treeDataProvider = new MetadataDiffTreeDataProvider(context); + + // Update the tree with imported data + await treeDataProvider.setDiffFiles(report.files); + + // Register the new tree data provider + context.subscriptions.push( + vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) + ); + + vscode.window.showInformationMessage("Report imported successfully"); + } + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_REPORT_FAILED, + error as string, + error as Error + ); + vscode.window.showErrorMessage("Failed to import metadata diff report"); + } + }); +} diff --git a/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts b/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts new file mode 100644 index 000000000..df2eb16d7 --- /dev/null +++ b/src/client/power-pages/metadata-diff/MetadataDiffDesktop.ts @@ -0,0 +1,118 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import * as fs from "fs"; +import { ECSFeaturesClient } from "../../../common/ecs-features/ecsFeatureClient"; +import { EnableMetadataDiff } from "../../../common/ecs-features/ecsFeatureGates"; +import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; +import { Constants } from "../../../common/power-pages/metadata-diff/Constants"; +import { MetadataDiffTreeDataProvider } from "../../../common/power-pages/metadata-diff/MetadataDiffTreeDataProvider"; +import { PacTerminal } from "../../lib/PacTerminal"; +import { PagesList } from "../../pac/PacTypes"; +import { registerMetadataDiffCommands } from "./MetadataDiffCommands"; + +export class MetadataDiffDesktop { + private static _isInitialized = false; + private static _treeDataProvider: MetadataDiffTreeDataProvider | undefined; + + static isEnabled(): boolean { + const enableMetadataDiff = ECSFeaturesClient.getConfig(EnableMetadataDiff).enableMetadataDiff + + if (enableMetadataDiff === undefined) { + return false; + } + + return enableMetadataDiff; + } + + static resetTreeView(): void { + if (this._treeDataProvider) { + this._treeDataProvider.clearItems(); + // Force reset tree view context to show welcome message + vscode.commands.executeCommand("setContext", "microsoft.powerplatform.pages.metadataDiff.hasData", false); + } + } + + static async initialize(context: vscode.ExtensionContext, pacTerminal: PacTerminal): Promise { + if (MetadataDiffDesktop._isInitialized) { + return; + } + + try { + const isMetadataDiffEnabled = MetadataDiffDesktop.isEnabled(); + + oneDSLoggerWrapper.getLogger().traceInfo("EnableMetadataDiff", { + isEnabled: isMetadataDiffEnabled.toString() + }); + + vscode.commands.executeCommand("setContext", "microsoft.powerplatform.pages.metadataDiffEnabled", isMetadataDiffEnabled); + + if (!isMetadataDiffEnabled) { + return; + } + + const storagePath = context.storageUri?.fsPath; + if (!storagePath) { + throw new Error("Storage path is not defined"); + } + if (fs.existsSync(storagePath)) { + fs.rmSync(storagePath, { recursive: true, force: true }); + } + fs.mkdirSync(storagePath, { recursive: true }); + + await registerMetadataDiffCommands(context, pacTerminal); + + const treeDataProvider = MetadataDiffTreeDataProvider.initialize(context); + MetadataDiffDesktop._treeDataProvider = treeDataProvider; + context.subscriptions.push( + vscode.window.registerTreeDataProvider("microsoft.powerplatform.pages.metadataDiff", treeDataProvider) + ); + + MetadataDiffDesktop._isInitialized = true; + oneDSLoggerWrapper.getLogger().traceInfo(Constants.EventNames.METADATA_DIFF_INITIALIZED); + } catch (exception) { + const exceptionError = exception as Error; + oneDSLoggerWrapper.getLogger().traceError(Constants.EventNames.METADATA_DIFF_INITIALIZATION_FAILED, exceptionError.message, exceptionError); + } + } + + static async getPagesList(pacTerminal: PacTerminal): Promise<{ name: string, id: string, modelVersion: string }[]> { + const pacWrapper = pacTerminal.getWrapper(); + const pagesListOutput = await pacWrapper.pagesList(); + if (pagesListOutput && pagesListOutput.Status === "Success" && pagesListOutput.Information) { + // Parse the list of pages from the string output + const pagesList: PagesList[] = []; + if (Array.isArray(pagesListOutput.Information)) { + // If Information is already an array of strings + pagesListOutput.Information.forEach(line => { + // Skip empty lines or header lines + if (!line.trim() || !line.includes('[')) { + return; + } + + // Extract the relevant parts using regex + const match = line.match(/\[\d+\]\s+([a-f0-9-]+)\s+(.*?)\s+(v[12])\s*$/i); + if (match) { + pagesList.push({ + WebsiteId: match[1].trim(), + FriendlyName: match[2].trim(), + ModelVersion: match[3].trim() + }); + } + }); + } + return pagesList.map((site) => { + return { + name: site.FriendlyName, + id: site.WebsiteId, + modelVersion: site.ModelVersion + } + }); + } + + return []; + } +} diff --git a/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts b/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts new file mode 100644 index 000000000..e452c4b70 --- /dev/null +++ b/src/client/power-pages/metadata-diff/MetadataDiffUtils.ts @@ -0,0 +1,237 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as fs from "fs"; +import * as path from "path"; +import * as yaml from 'yaml'; + +export interface DiffFile { + relativePath: string; + changes: string; + type: string; + workspaceContent?: string; + storageContent?: string; +} + +export interface MetadataDiffReport { + generatedOn: string; + files: DiffFile[]; +} + +export async function generateDiffReport(workspacePath: string, storagePath: string): Promise { + let report = '# Power Pages Metadata Diff Report\n\n'; + report += `Generated on: ${new Date().toLocaleString()}\n\n`; + + // Get diff files + const diffFiles = await getAllDiffFiles(workspacePath, storagePath); + + // Group by folders + const folderStructure = new Map(); + diffFiles.forEach(file => { + const dirPath = path.dirname(file.relativePath); + if (!folderStructure.has(dirPath)) { + folderStructure.set(dirPath, []); + } + folderStructure.get(dirPath)!.push(file); + }); + + // Sort folders and generate report + const sortedFolders = Array.from(folderStructure.keys()).sort(); + for (let i = 0; i < sortedFolders.length; i++) { + const folder = sortedFolders[i]; + const files = folderStructure.get(folder)!; + + // Add folder header (skip for root) + if (folder !== '.') { + report += `## ${folder}\n\n`; + } + + // Add files + for (const file of files.sort((a, b) => a.relativePath.localeCompare(b.relativePath))) { + report += `- ${path.basename(file.relativePath)}\n`; + report += ` - Changes: ${file.changes}\n`; + + // Add detailed YAML property changes for YAML files + if (path.extname(file.relativePath).toLowerCase() === '.yml' || + path.extname(file.relativePath).toLowerCase() === '.yaml') { + try { + if (file.workspaceContent && file.storageContent && file.changes === 'Modified') { + const workspaceYaml = yaml.parse(file.workspaceContent); + const storageYaml = yaml.parse(file.storageContent); + + const propertyChanges = findYamlPropertyChanges(workspaceYaml, storageYaml); + + if (propertyChanges.length > 0) { + report += ` - Property changes:\n`; + propertyChanges.forEach(change => { + report += ` - ${change}\n`; + }); + } + } + } catch (error) { + report += ` - Failed to parse YAML content: ${error}\n`; + } + } + } + + // Only add a blank line between folders, not after the last folder + if (i < sortedFolders.length - 1) { + report += '\n'; + } + } + + return report; +} + +/** + * Finds differences between two YAML objects and returns a list of changes + * @param workspaceObj The workspace YAML object + * @param storageObj The storage YAML object + * @returns An array of change descriptions + */ +function findYamlPropertyChanges(workspaceObj: any, storageObj: any, path = ''): string[] { + if (!workspaceObj || !storageObj) { + return []; + } + + const changes: string[] = []; + + // Check all properties in workspace object + const workspaceKeys = Object.keys(workspaceObj); + for (const key of workspaceKeys) { + const currentPath = path ? `${path}.${key}` : key; + + // Check if key exists in storage + if (!(key in storageObj)) { + changes.push(`Added property: \`${currentPath}\``); + continue; + } + + // Check if both values are objects (for nested properties) + if (typeof workspaceObj[key] === 'object' && workspaceObj[key] !== null && + typeof storageObj[key] === 'object' && storageObj[key] !== null) { + // Recursively check nested objects + const nestedChanges = findYamlPropertyChanges( + workspaceObj[key], + storageObj[key], + currentPath + ); + changes.push(...nestedChanges); + } + // Check primitive values (string, number, boolean, etc.) + else if (JSON.stringify(workspaceObj[key]) !== JSON.stringify(storageObj[key])) { + changes.push(`Modified property: \`${currentPath}\``); + } + } + + // Check for properties that exist in storage but not in workspace + const storageKeys = Object.keys(storageObj); + for (const key of storageKeys) { + const currentPath = path ? `${path}.${key}` : key; + if (!(key in workspaceObj)) { + changes.push(`Removed property: \`${currentPath}\``); + } + } + + return changes; +} + +export async function getAllDiffFiles(workspacePath: string, storagePath: string): Promise { + const diffFiles: DiffFile[] = []; + + // Get website directory from storage path + const folderNames = fs.readdirSync(storagePath).filter(file => + fs.statSync(path.join(storagePath, file)).isDirectory() + ); + if (folderNames.length === 0) { + return diffFiles; + } + const websitePath = path.join(storagePath, folderNames[0]); + + // Get all files + const workspaceFiles = await getAllFiles(workspacePath); + const storageFiles = await getAllFiles(websitePath); + + // Create normalized path maps + const workspaceMap = new Map(workspaceFiles.map(f => { + const normalized = path.relative(workspacePath, f).replace(/\\/g, '/'); + return [normalized, f]; + })); + const storageMap = new Map(storageFiles.map(f => { + const normalized = path.relative(websitePath, f).replace(/\\/g, '/'); + return [normalized, f]; + })); + + // Compare files + for (const [normalized, workspaceFile] of workspaceMap.entries()) { + const storageFile = storageMap.get(normalized); + if (!storageFile) { + diffFiles.push({ + relativePath: normalized, + changes: 'Only in workspace', + type: path.dirname(normalized) || 'Other', + workspaceContent: fs.readFileSync(workspaceFile, 'utf8').replace(/\r\n/g, '\n') + }); + continue; + } + + // Compare content + const workspaceContent = fs.readFileSync(workspaceFile, 'utf8').replace(/\r\n/g, '\n'); + const storageContent = fs.readFileSync(storageFile, 'utf8').replace(/\r\n/g, '\n'); + if (workspaceContent !== storageContent) { + diffFiles.push({ + relativePath: normalized, + changes: 'Modified', + type: path.dirname(normalized) || 'Other', + workspaceContent, + storageContent + }); + } + } + + // Check for files only in storage + for (const [normalized, storageFile] of storageMap.entries()) { + if (!workspaceMap.has(normalized)) { + diffFiles.push({ + relativePath: normalized, + changes: 'Only in remote', + type: path.dirname(normalized) || 'Other', + storageContent: fs.readFileSync(storageFile, 'utf8').replace(/\r\n/g, '\n') + }); + } + } + + return diffFiles; +} + +export async function getAllFiles(dirPath: string): Promise { + const files: string[] = []; + + function traverse(currentPath: string) { + const entries = fs.readdirSync(currentPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + if (entry.isDirectory()) { + traverse(fullPath); + } else { + files.push(fullPath); + } + } + } + + traverse(dirPath); + return files; +} + +export function groupDiffsByType(files: DiffFile[]): Record { + return files.reduce((groups: Record, file) => { + const type = file.type || 'Other'; + if (!groups[type]) { + groups[type] = []; + } + groups[type].push(file); + return groups; + }, {}); +} diff --git a/src/common/ecs-features/ecsFeatureGates.ts b/src/common/ecs-features/ecsFeatureGates.ts index aa09ffea2..fd0a33924 100644 --- a/src/common/ecs-features/ecsFeatureGates.ts +++ b/src/common/ecs-features/ecsFeatureGates.ts @@ -68,3 +68,13 @@ export const { enableActionsHub: false, }, }); + +export const { + feature: EnableMetadataDiff +} = getFeatureConfigs({ + teamName: PowerPagesClientName, + description: 'Enable Metadata Diff comparison in VS Code', + fallback: { + enableMetadataDiff: false, + }, +}); diff --git a/src/common/power-pages/metadata-diff/Constants.ts b/src/common/power-pages/metadata-diff/Constants.ts new file mode 100644 index 000000000..4c19af137 --- /dev/null +++ b/src/common/power-pages/metadata-diff/Constants.ts @@ -0,0 +1,18 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +export const Constants = { + EventNames: { + METADATA_DIFF_INITIALIZED: "metadataDiffInitialized", + METADATA_DIFF_REFRESH_FAILED: "metadataDiffRefreshFailed", + METADATA_DIFF_INITIALIZATION_FAILED: "metadataDiffInitializationFailed", + METADATA_DIFF_CURRENT_ENV_FETCH_FAILED: "metadataDiffCurrentEnvFetchFailed", + ORGANIZATION_URL_MISSING: "Organization URL is missing in the results.", + EMPTY_RESULTS_ARRAY: "Results array is empty or not an array.", + PAC_AUTH_OUTPUT_FAILURE: "pacAuthCreateOutput is missing or unsuccessful.", + METADATA_DIFF_REPORT_FAILED: "metadataDiffReportFailed" + } +}; +export const SUCCESS = "Success"; diff --git a/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts new file mode 100644 index 000000000..f00571da3 --- /dev/null +++ b/src/common/power-pages/metadata-diff/MetadataDiffTreeDataProvider.ts @@ -0,0 +1,300 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as path from "path"; +import { MetadataDiffTreeItem } from "./tree-items/MetadataDiffTreeItem"; +import { Constants } from "./Constants"; +import { oneDSLoggerWrapper } from "../../OneDSLoggerTelemetry/oneDSLoggerWrapper"; +import { MetadataDiffFileItem } from "./tree-items/MetadataDiffFileItem"; +import { MetadataDiffFolderItem } from "./tree-items/MetadataDiffFolderItem"; + +interface DiffFile { + relativePath: string; + changes: string; + type: string; + workspaceContent?: string; + storageContent?: string; +} + +export class MetadataDiffTreeDataProvider implements vscode.TreeDataProvider { + private readonly _disposables: vscode.Disposable[] = []; + private readonly _context: vscode.ExtensionContext; + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + private _diffItems: MetadataDiffTreeItem[] = []; + + constructor(context: vscode.ExtensionContext) { + this._context = context; + } + + public static initialize(context: vscode.ExtensionContext): MetadataDiffTreeDataProvider { + return new MetadataDiffTreeDataProvider(context); + } + + private refresh(): void { + this._onDidChangeTreeData.fire(); + } + + clearItems(): void { + this._diffItems = []; + // Reset any stored data + const storagePath = this._context.storageUri?.fsPath; + if (storagePath && fs.existsSync(storagePath)) { + try { + fs.rmSync(storagePath, { recursive: true, force: true }); + fs.mkdirSync(storagePath, { recursive: true }); + } catch (error) { + console.error('Error cleaning storage path:', error); + } + } + // Set context to show welcome message again + vscode.commands.executeCommand("setContext", "microsoft.powerplatform.pages.metadataDiff.hasData", false); + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element: MetadataDiffTreeItem): vscode.TreeItem | Thenable { + return element; + } + + private getAllFilesRecursively(dir: string, fileList: string[] = []): string[] { + const files = fs.readdirSync(dir); + + for (const file of files) { + const fullPath = path.join(dir, file); + if (fs.statSync(fullPath).isDirectory()) { + this.getAllFilesRecursively(fullPath, fileList); + } else { + fileList.push(fullPath); + } + } + + return fileList; + } + + private async getDiffFiles(workspacePath: string, storagePath: string): Promise> { + const diffFiles = new Map(); + + // Get website directory from storage path + const websitePath = await this.getWebsitePath(storagePath); + if (!websitePath) { + return diffFiles; + } + + const workspaceFiles = this.getAllFilesRecursively(workspacePath); + const storageFiles = this.getAllFilesRecursively(websitePath); + + // Create normalized path maps for comparison + const workspaceMap = new Map(workspaceFiles.map(f => { + const normalized = path.relative(workspacePath, f).replace(/\\/g, '/'); + return [normalized, f]; + })); + + const storageMap = new Map(storageFiles.map(f => { + const normalized = path.relative(websitePath, f).replace(/\\/g, '/'); + return [normalized, f]; + })); + + // Compare files + for (const [relativePath, workspaceFile] of workspaceMap.entries()) { + const storageFile = storageMap.get(relativePath); + + if (!storageFile) { + // File only exists in workspace + diffFiles.set(relativePath, { workspaceFile }); + continue; + } + + // Compare content only if both files exist + const workspaceContent = fs.readFileSync(workspaceFile, 'utf8').replace(/\r\n/g, '\n'); + const storageContent = fs.readFileSync(storageFile, 'utf8').replace(/\r\n/g, '\n'); + + if (workspaceContent !== storageContent) { + diffFiles.set(relativePath, { workspaceFile, storageFile }); + } + } + + // Check for files only in storage + for (const [relativePath, storageFile] of storageMap.entries()) { + if (!workspaceMap.has(relativePath)) { + diffFiles.set(relativePath, { storageFile }); + } + } + + return diffFiles; + } + + private async getWebsitePath(storagePath: string): Promise { + try { + const folders = fs.readdirSync(storagePath).filter(f => + fs.statSync(path.join(storagePath, f)).isDirectory() + ); + + if (folders.length > 0) { + return path.join(storagePath, folders[0]); + } + } catch (error) { + console.error('Error finding website path:', error); + } + return undefined; + } + + async getChildren(element?: MetadataDiffTreeItem): Promise { + if (element) { + return element.getChildren(); + } + + // If we have imported diff items, return those + if (this._diffItems && this._diffItems.length > 0) { + return this._diffItems; + } + + try { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders) { + return []; + } + + const workspacePath = workspaceFolders[0].uri.fsPath; + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) { + throw new Error("Storage path is not defined"); + } + + const diffFiles = await this.getDiffFiles(workspacePath, storagePath); + if (diffFiles.size === 0) { + return []; + } + + return this.buildTreeHierarchy(diffFiles); + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + Constants.EventNames.METADATA_DIFF_CURRENT_ENV_FETCH_FAILED, + error as string, + error as Error, + { methodName: this.getChildren }, + {} + ); + return null; + } + } + + private buildTreeHierarchy(filePathMap: Map): MetadataDiffTreeItem[] { + const rootNode = new MetadataDiffFolderItem(''); + + for (const [relativePath, { workspaceFile, storageFile }] of filePathMap.entries()) { + const parts = relativePath.split('/'); + let currentNode = rootNode; + + // Create folder hierarchy + for (let i = 0; i < parts.length - 1; i++) { + const folderName = parts[i]; + let folderNode = currentNode.getChildrenMap().get(folderName) as MetadataDiffFolderItem; + + if (!folderNode) { + folderNode = new MetadataDiffFolderItem(folderName); + currentNode.getChildrenMap().set(folderName, folderNode); + } + + currentNode = folderNode; + } + + // Add file + const fileName = parts[parts.length - 1]; + const fileNode = new MetadataDiffFileItem( + fileName, + workspaceFile, + storageFile, + true + ); + fileNode.description = this.getChangeDescription(workspaceFile, storageFile); + currentNode.getChildrenMap().set(fileName, fileNode); + } + + // Convert the root's children map to array + return Array.from(rootNode.getChildrenMap().values()); + } + + private getChangeDescription(workspaceFile?: string, storageFile?: string): string { + if (!workspaceFile) return 'Only in remote'; + if (!storageFile) return 'Only in workspace'; + return 'Modified'; + } + + async setDiffFiles(files: DiffFile[]): Promise { + const rootNode = new MetadataDiffFolderItem(''); + const sortedFiles = files.sort((a, b) => a.relativePath.localeCompare(b.relativePath)); + + // Get temp directory for storing imported file contents + const tempDir = path.join(this._context.storageUri?.fsPath || '', 'imported_diff'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + for (const file of sortedFiles) { + const parts = file.relativePath.split('/'); + let currentNode = rootNode; + + // Create folder hierarchy + for (let i = 0; i < parts.length - 1; i++) { + const folderName = parts[i]; + let folderNode = currentNode.getChildrenMap().get(folderName) as MetadataDiffFolderItem; + + if (!folderNode) { + // Create folder with expanded state in constructor + folderNode = new MetadataDiffFolderItem(folderName, vscode.TreeItemCollapsibleState.Expanded); + folderNode.iconPath = new vscode.ThemeIcon("folder"); + currentNode.getChildrenMap().set(folderName, folderNode); + } + + currentNode = folderNode; + } + + // Rest of the file handling code... + const fileName = parts[parts.length - 1]; + const filePath = path.join(tempDir, file.relativePath); + const dirPath = path.dirname(filePath); + fs.mkdirSync(dirPath, { recursive: true }); + + // Extract original file extension to preserve it + const fileExt = path.extname(fileName); + + let workspaceFilePath: string | undefined; + let storageFilePath: string | undefined; + + if (file.workspaceContent) { + // Use the original file extension in the temp file to preserve language identification + workspaceFilePath = `${filePath}.workspace${fileExt}`; + fs.writeFileSync(workspaceFilePath, file.workspaceContent); + } + + if (file.storageContent) { + // Use the original file extension in the temp file to preserve language identification + storageFilePath = `${filePath}.storage${fileExt}`; + fs.writeFileSync(storageFilePath, file.storageContent); + } + + const fileNode = new MetadataDiffFileItem( + fileName, + workspaceFilePath, + storageFilePath, + true + ); + fileNode.description = file.changes; + fileNode.iconPath = new vscode.ThemeIcon("file"); + fileNode.command = { + command: 'metadataDiff.openDiff', + title: 'Show Diff', + arguments: [workspaceFilePath, storageFilePath] + }; + + currentNode.getChildrenMap().set(fileName, fileNode); + } + + this._diffItems = Array.from(rootNode.getChildrenMap().values()); + this._onDidChangeTreeData.fire(); + } +} diff --git a/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts new file mode 100644 index 000000000..f3fae460f --- /dev/null +++ b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFileItem.ts @@ -0,0 +1,35 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import { MetadataDiffTreeItem } from "./MetadataDiffTreeItem"; + +export class MetadataDiffFileItem extends MetadataDiffTreeItem { + constructor(label: string, workspaceFile?: string, storageFile?: string, hasDiff = true) { + super( + label, + vscode.TreeItemCollapsibleState.None, + "metadataDiffFileItem", + workspaceFile, + storageFile + ); + this.workspaceFile = workspaceFile; + this.storageFile = storageFile; + this.hasDiff = hasDiff; + this.iconPath = new vscode.ThemeIcon("file"); + + if (hasDiff && (workspaceFile || storageFile)) { + this.command = { + command: 'metadataDiff.openDiff', + title: 'Show Diff', + arguments: [workspaceFile, storageFile] + }; + } + } + + public readonly workspaceFile?: string; + public readonly storageFile?: string; + public readonly hasDiff: boolean; +} diff --git a/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFolderItem.ts b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFolderItem.ts new file mode 100644 index 000000000..c2baeccbb --- /dev/null +++ b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffFolderItem.ts @@ -0,0 +1,14 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import { MetadataDiffTreeItem } from "./MetadataDiffTreeItem"; + +export class MetadataDiffFolderItem extends MetadataDiffTreeItem { + constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState = vscode.TreeItemCollapsibleState.Expanded) { + super(label, collapsibleState, "folder"); + this.iconPath = new vscode.ThemeIcon("folder"); + } +} diff --git a/src/common/power-pages/metadata-diff/tree-items/MetadataDiffTreeItem.ts b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffTreeItem.ts new file mode 100644 index 000000000..6ab0b9442 --- /dev/null +++ b/src/common/power-pages/metadata-diff/tree-items/MetadataDiffTreeItem.ts @@ -0,0 +1,42 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; + +export class MetadataDiffTreeItem extends vscode.TreeItem { + private _childrenMap: Map; + + constructor( + public readonly label: string, + public readonly collapsibleState: vscode.TreeItemCollapsibleState, + public readonly contextValue?: string, + public readonly filePath?: string, // Workspace file path + public readonly storedFilePath?: string // Backup copy path + ) { + super(label, collapsibleState); + this._childrenMap = new Map(); + this.contextValue = contextValue; + this.iconPath = collapsibleState === vscode.TreeItemCollapsibleState.None ? + new vscode.ThemeIcon("file") : + new vscode.ThemeIcon("folder"); + this.tooltip = this.label; + + if (filePath && storedFilePath) { + this.command = { + command: "metadataDiff.openDiff", + title: "Open Diff", + arguments: [filePath, storedFilePath] // Pass file paths to the command + }; + } + } + + public getChildren(): MetadataDiffTreeItem[] { + return Array.from(this._childrenMap.values()); + } + + public getChildrenMap(): Map { + return this._childrenMap; + } +} diff --git a/src/common/utilities/PacAuthUtil.ts b/src/common/utilities/PacAuthUtil.ts index 78305a5c4..1c5ad9878 100644 --- a/src/common/utilities/PacAuthUtil.ts +++ b/src/common/utilities/PacAuthUtil.ts @@ -10,22 +10,25 @@ import { AUTH_CREATE_FAILED, AUTH_CREATE_MESSAGE } from "../copilot/constants"; import { showInputBoxAndGetOrgUrl, showProgressWithNotification } from "./Utils"; - export async function createAuthProfileExp(pacWrapper: PacWrapper | undefined) { - const userOrgUrl = await showInputBoxAndGetOrgUrl(); - if (!userOrgUrl) { - return; - } + export async function createAuthProfileExp(pacWrapper: PacWrapper | undefined, userOrgUrl = "") { + if (!userOrgUrl) { + userOrgUrl = await showInputBoxAndGetOrgUrl() ?? ""; + } - if (!pacWrapper) { - vscode.window.showErrorMessage(AUTH_CREATE_FAILED); - return; - } + if (!userOrgUrl) { + return; + } - const pacAuthCreateOutput = await showProgressWithNotification(vscode.l10n.t(AUTH_CREATE_MESSAGE), async () => { return await pacWrapper?.authCreateNewAuthProfileForOrg(userOrgUrl) }); - if (pacAuthCreateOutput && pacAuthCreateOutput.Status !== SUCCESS) { - vscode.window.showErrorMessage(AUTH_CREATE_FAILED); - return; - } + if (!pacWrapper) { + vscode.window.showErrorMessage(AUTH_CREATE_FAILED); + return; + } + + const pacAuthCreateOutput = await showProgressWithNotification(vscode.l10n.t(AUTH_CREATE_MESSAGE), async () => { return await pacWrapper?.authCreateNewAuthProfileForOrg(userOrgUrl) }); + if (pacAuthCreateOutput && pacAuthCreateOutput.Status !== SUCCESS) { + vscode.window.showErrorMessage(AUTH_CREATE_FAILED); + return; + } return pacAuthCreateOutput; }