From 4c97bf4a3250c71daf3b03c3318f83aec5ffda84 Mon Sep 17 00:00:00 2001 From: Elinor Date: Thu, 13 Jun 2024 16:05:31 +0300 Subject: [PATCH] Task: Display client/plugin name on API explorer (#4801) --- vscode/microsoft-kiota/package.json | 8 +- vscode/microsoft-kiota/src/extension.ts | 19 +- vscode/microsoft-kiota/src/kiotaInterop.ts | 26 +-- .../src/openApiTreeProvider.ts | 211 +++++++++++++----- 4 files changed, 185 insertions(+), 79 deletions(-) diff --git a/vscode/microsoft-kiota/package.json b/vscode/microsoft-kiota/package.json index 07df58710c..d5c8c0a356 100644 --- a/vscode/microsoft-kiota/package.json +++ b/vscode/microsoft-kiota/package.json @@ -271,22 +271,22 @@ }, { "command": "kiota.openApiExplorer.addToSelectedEndpoints", - "when": "view == kiota.openApiExplorer && viewItem != apiTitle", + "when": "view == kiota.openApiExplorer && viewItem != apiTitle && viewItem != clientNameOrPluginName", "group": "inline@2" }, { "command": "kiota.openApiExplorer.addAllToSelectedEndpoints", - "when": "view == kiota.openApiExplorer", + "when": "view == kiota.openApiExplorer && viewItem != clientNameOrPluginName", "group": "inline@4" }, { "command": "kiota.openApiExplorer.removeFromSelectedEndpoints", - "when": "view == kiota.openApiExplorer && viewItem != apiTitle", + "when": "view == kiota.openApiExplorer && viewItem != apiTitle && viewItem != clientNameOrPluginName", "group": "inline@3" }, { "command": "kiota.openApiExplorer.removeAllFromSelectedEndpoints", - "when": "view == kiota.openApiExplorer", + "when": "view == kiota.openApiExplorer && viewItem != clientNameOrPluginName", "group": "inline@5" } ], diff --git a/vscode/microsoft-kiota/src/extension.ts b/vscode/microsoft-kiota/src/extension.ts index 05f3069d17..66b36ff9b0 100644 --- a/vscode/microsoft-kiota/src/extension.ts +++ b/vscode/microsoft-kiota/src/extension.ts @@ -45,6 +45,7 @@ export async function activate( kiotaOutputChannel = vscode.window.createOutputChannel("Kiota", { log: true, }); + const workspaceJsonPath = path.join(vscode.workspace.workspaceFolders?.map(folder => folder.uri.fsPath).join('') || '', KIOTA_DIRECTORY, KIOTA_WORKSPACE_FILE); const openApiTreeProvider = new OpenApiTreeProvider(context, () => getExtensionSettings(extensionId)); const dependenciesInfoProvider = new DependenciesViewProvider( context.extensionUri @@ -225,7 +226,7 @@ export async function activate( clientOrPluginKey = clientKey; clientOrPluginObject = clientObject; workspaceGenerationType = generationType; - await loadEditPaths(clientObject, openApiTreeProvider); + await loadEditPaths(clientOrPluginKey, clientObject, openApiTreeProvider); await vscode.commands.executeCommand('setContext',`${treeViewId}.showIcons`, false); await vscode.commands.executeCommand('setContext', `${treeViewId}.showRegenerateIcon`, true); }), @@ -300,6 +301,8 @@ export async function activate( if (result) { await checkForSuccess(result); + openApiTreeProvider.refreshView(); + await loadLockFile({fsPath: workspaceJsonPath}, openApiTreeProvider, config.pluginName); await exportLogsAndShowErrors(result); } } @@ -339,6 +342,8 @@ export async function activate( if (result) { await checkForSuccess(result); + openApiTreeProvider.refreshView(); + await loadLockFile({fsPath: workspaceJsonPath}, openApiTreeProvider, config.pluginName); await exportLogsAndShowErrors(result); } } @@ -402,11 +407,13 @@ export async function activate( result && getLogEntriesForLevel(result, LogLevel.critical, LogLevel.error).length === 0) { const WORKSPACE_FOLDER = vscode.workspace.workspaceFolders[0].uri.fsPath; const KIOTA_WORKSPACE_PATH = path.join(WORKSPACE_FOLDER, KIOTA_DIRECTORY, KIOTA_WORKSPACE_FILE); - await openApiTreeProvider.loadLockFile(KIOTA_WORKSPACE_PATH); + await openApiTreeProvider.loadLockFile(KIOTA_WORKSPACE_PATH, config.clientClassName); } if (result) { await checkForSuccess(result); + openApiTreeProvider.refreshView(); + await loadLockFile({fsPath: workspaceJsonPath}, openApiTreeProvider, config.clientClassName); await exportLogsAndShowErrors(result); } } @@ -574,13 +581,13 @@ async function showUpgradeWarningMessage(clientPath: string, context: vscode.Ext } } -async function loadLockFile(node: { fsPath: string }, openApiTreeProvider: OpenApiTreeProvider): Promise { - await openTreeViewWithProgress(() => openApiTreeProvider.loadLockFile(node.fsPath)); +async function loadLockFile(node: { fsPath: string }, openApiTreeProvider: OpenApiTreeProvider, clientOrPluginName?: string): Promise { + await openTreeViewWithProgress(() => openApiTreeProvider.loadLockFile(node.fsPath, clientOrPluginName)); await vscode.commands.executeCommand('setContext',`${treeViewId}.showIcons`, true); } -async function loadEditPaths(clientObject: any, openApiTreeProvider: OpenApiTreeProvider): Promise { - await openTreeViewWithProgress(() => openApiTreeProvider.loadEditPaths(clientObject)); +async function loadEditPaths(clientOrPluginKey: string, clientObject: any, openApiTreeProvider: OpenApiTreeProvider): Promise { + await openTreeViewWithProgress(() => openApiTreeProvider.loadEditPaths(clientOrPluginKey, clientObject)); } async function exportLogsAndShowErrors(result: KiotaLogEntry[]) : Promise { diff --git a/vscode/microsoft-kiota/src/kiotaInterop.ts b/vscode/microsoft-kiota/src/kiotaInterop.ts index 974544ebfd..01a862cd72 100644 --- a/vscode/microsoft-kiota/src/kiotaInterop.ts +++ b/vscode/microsoft-kiota/src/kiotaInterop.ts @@ -41,6 +41,7 @@ export interface KiotaOpenApiNode { selected?: boolean, isOperation?: boolean; documentationUrl?: string; + clientNameOrPluginName?: string; } interface CacheClearableConfiguration { clearCache: boolean; @@ -253,23 +254,10 @@ export function maturityLevelToString(level: MaturityLevel): string { throw new Error("unknown level"); } } -export interface LockFile { - clientClassName: string; - clientNamespaceName: string; - descriptionHash: string; - descriptionLocation: string; - deserializers: string[]; - disabledValidationRules: string[]; - excludeBackwardCompatible: boolean; - excludePatterns: string[]; - includeAdditionalData: boolean; - includePatterns: string[]; - kiotaVersion: string; - language: string; - lockFileVersion: string; - serializers: string[]; - structuredMimeTypes: string[]; - usesBackingStore: boolean; +export interface ConfigurationFile { + version: string; + clients: Record; + plugins: Record; } export interface GenerationConfiguration { @@ -300,7 +288,7 @@ interface WorkspaceObjectProperties { outputPath: string; } -interface ClientObjectProperties extends WorkspaceObjectProperties { +export interface ClientObjectProperties extends WorkspaceObjectProperties { language: string; structuredMimeTypes: string[]; clientNamespaceName: string; @@ -310,7 +298,7 @@ interface ClientObjectProperties extends WorkspaceObjectProperties { disabledValidationRules: string[]; } -interface PluginObjectProperties extends WorkspaceObjectProperties { +export interface PluginObjectProperties extends WorkspaceObjectProperties { types: string[]; } diff --git a/vscode/microsoft-kiota/src/openApiTreeProvider.ts b/vscode/microsoft-kiota/src/openApiTreeProvider.ts index bd26850ddf..2d34ad150e 100644 --- a/vscode/microsoft-kiota/src/openApiTreeProvider.ts +++ b/vscode/microsoft-kiota/src/openApiTreeProvider.ts @@ -3,7 +3,18 @@ import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; import * as rpc from 'vscode-jsonrpc/node'; -import { connectToKiota, KiotaGetManifestDetailsConfiguration, KiotaLogEntry, KiotaManifestResult, KiotaOpenApiNode, KiotaShowConfiguration, KiotaShowResult, LockFile } from './kiotaInterop'; +import { + ClientObjectProperties, + ClientOrPluginProperties, + connectToKiota, + KiotaGetManifestDetailsConfiguration, + KiotaLogEntry, + KiotaManifestResult, + KiotaOpenApiNode, + KiotaShowConfiguration, + KiotaShowResult, + ConfigurationFile, + PluginObjectProperties } from './kiotaInterop'; import { ExtensionSettings } from './extensionSettings'; import { treeViewId } from './constants'; @@ -13,47 +24,83 @@ export class OpenApiTreeProvider implements vscode.TreeDataProvider ExtensionSettings, + private readonly settingsGetter: () => ExtensionSettings, private _descriptionUrl?: string, public includeFilters: string[] = [], public excludeFilters: string[] = []) { - + } private _lockFilePath?: string; - private _lockFile?: LockFile; + private _lockFile?: ConfigurationFile | Partial = {}; public get isLockFileLoaded(): boolean { return !!this._lockFile; } - public async loadLockFile(path: string): Promise { - this.closeDescription(false); - this._lockFilePath = path; - const lockFileData = await vscode.workspace.fs.readFile(vscode.Uri.file(path)); - this._lockFile = JSON.parse(lockFileData.toString()) as LockFile; - if (this._lockFile?.descriptionLocation) { - this._descriptionUrl = this._lockFile.descriptionLocation; - this.includeFilters = this._lockFile.includePatterns; - this.excludeFilters = this._lockFile.excludePatterns; - const settings = this.settingsGetter(); - await this.loadNodes(settings.clearCache); - if (this.rawRootNode) { - this.refreshView(); + public async loadLockFile(path: string, clientOrPluginName?: string): Promise { + this.closeDescription(false); + this._lockFilePath = path; + const lockFileData = await vscode.workspace.fs.readFile(vscode.Uri.file(path)); + let parsedLockFile = JSON.parse(lockFileData.toString()) as ConfigurationFile; + + if (clientOrPluginName) { + let filteredData: Partial = { version: parsedLockFile.version }; + + if (parsedLockFile.clients && parsedLockFile.clients[clientOrPluginName]) { + filteredData.clients = { + [clientOrPluginName]: parsedLockFile.clients[clientOrPluginName] + }; + } + + if (parsedLockFile.plugins && parsedLockFile.plugins[clientOrPluginName]) { + filteredData.plugins = { + [clientOrPluginName]: parsedLockFile.plugins[clientOrPluginName] + }; + } + + parsedLockFile = filteredData as ConfigurationFile; + } + + this._lockFile = parsedLockFile; + + const clientOrPlugin: ClientOrPluginProperties | undefined = + Object.values(this._lockFile.clients ?? {})[0] || + Object.values(this._lockFile.plugins ?? {})[0]; + + if (clientOrPlugin) { + this._descriptionUrl = clientOrPlugin.descriptionLocation; + this.includeFilters = clientOrPlugin.includePatterns; + this.excludeFilters = clientOrPlugin.excludePatterns; + + const settings = this.settingsGetter(); + await this.loadNodes(settings.clearCache, clientOrPluginName); + + if (this.rawRootNode) { + this.refreshView(); + } } - } } - public async loadEditPaths(clientObject: any): Promise { + public async loadEditPaths(clientOrPluginKey: string, clientObject: ClientOrPluginProperties): Promise { this.closeDescription(false); - this._lockFile = clientObject; - if (this._lockFile?.descriptionLocation) { - this._descriptionUrl = this._lockFile.descriptionLocation; - this.includeFilters = this._lockFile.includePatterns; - this.excludeFilters = this._lockFile.excludePatterns; - const settings = this.settingsGetter(); - await this.loadNodes(settings.clearCache); - if (this.rawRootNode) { - this.refreshView(); - } + const newLockFile: ConfigurationFile = { version: '1.0.0', clients: {}, plugins: {} }; + + if ((clientObject as ClientObjectProperties).clientNamespaceName) { + newLockFile.clients[clientOrPluginKey] = clientObject as ClientObjectProperties; + } else { + newLockFile.plugins[clientOrPluginKey] = clientObject as PluginObjectProperties; + } + this._lockFile = newLockFile; + if (clientObject.descriptionLocation) { + this._descriptionUrl = clientObject.descriptionLocation; + this.includeFilters = clientObject.includePatterns; + this.excludeFilters = clientObject.excludePatterns; + + const settings = this.settingsGetter(); + await this.loadNodes(settings.clearCache, clientOrPluginKey); + + if (this.rawRootNode) { + this.refreshView(); + } } - } + } public async loadManifestFromUri(path: string, apiIdentifier?: string): Promise { this.closeDescription(false); const logs = await this.loadNodesFromManifest(path, apiIdentifier); @@ -73,17 +120,34 @@ export class OpenApiTreeProvider implements vscode.TreeDataProvider this.setAllSelected(x, selected)); } + private getFirstClient(): ClientObjectProperties | undefined { + return this._lockFile?.clients ? Object.values(this._lockFile.clients)[0] : undefined; + } public get outputPath(): string { - return this._lockFilePath ? path.parse(this._lockFilePath).dir : ''; + return this._lockFilePath ? path.parse(this._lockFilePath).dir : ''; } public get clientClassName(): string { - return this._lockFile?.clientClassName ?? ''; + if (this._lockFile?.clients) { + const client = this.getFirstClient(); + return client ? client.clientNamespaceName : ''; + } + return ''; } + public get clientNamespaceName(): string { - return this._lockFile?.clientNamespaceName ?? ''; + if (this._lockFile?.clients) { + const client = this.getFirstClient(); + return client ? client.clientNamespaceName : ''; + } + return ''; } + public get language(): string { - return this._lockFile?.language ?? ''; + if (this._lockFile?.clients) { + const client = this.getFirstClient(); + return client ? client.language : ''; + } + return ''; } public closeDescription(shouldRefresh = true) { this._descriptionUrl = ''; @@ -92,10 +156,12 @@ export class OpenApiTreeProvider implements vscode.TreeDataProvider this.selectInternal(x, selected, recursive)); } else if (!isOperationNode) { apiNode.children.filter(x => x.isOperation ?? false).forEach(x => this.selectInternal(x, selected, false)); @@ -140,7 +206,15 @@ export class OpenApiTreeProvider implements vscode.TreeDataProvider x.segment === segment); + let child: KiotaOpenApiNode | undefined; + if (currentNode.clientNameOrPluginName) { + const rootChild = currentNode.children.find(x => x.segment === '/'); + if (rootChild) { + child = rootChild.children.find(x => x.segment === segment); + } + } else { + child = currentNode.children.find(x => x.segment === segment); + } if (child) { return this.findApiNode(segments, child); } else if (segment.startsWith('{') && segment.endsWith('}')) { @@ -168,7 +242,7 @@ export class OpenApiTreeProvider implements vscode.TreeDataProvider { + private async loadNodes(clearCache: boolean, clientNameOrPluginName?: string): Promise { if (!this.descriptionUrl || this.descriptionUrl.length === 0) { return; } @@ -226,24 +300,35 @@ export class OpenApiTreeProvider implements vscode.TreeDataProvider this.getTreeNodeFromKiotaNode(x)), node.documentationUrl, + node.clientNameOrPluginName ); } getChildren(element?: OpenApiTreeNode): vscode.ProviderResult { @@ -279,6 +365,26 @@ function getPathSegments(path: string): string[] { function trimOperation(path: string): string { return path.split(operationSeparator)[0]; } + +function createKiotaOpenApiNode( + segment: string, + path: string, + children: KiotaOpenApiNode[] = [], + selected?: boolean, + isOperation?: boolean, + documentationUrl?: string, + clientNameOrPluginName?: string +): KiotaOpenApiNode { + return { + segment, + path, + children, + selected, + isOperation, + documentationUrl, + clientNameOrPluginName + }; +} type IconSet = string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri } | vscode.ThemeIcon; export class OpenApiTreeNode extends vscode.TreeItem { private static readonly selectedSet: IconSet = new vscode.ThemeIcon('check'); @@ -293,12 +399,17 @@ export class OpenApiTreeNode extends vscode.TreeItem { filterTokens: string[], apiTitle: string | undefined, public readonly children: OpenApiTreeNode[] = [], - public readonly documentationUrl?: string + public readonly documentationUrl?: string, + public readonly clientNameOrPluginName?: string ) { super(label, collapsibleState); this.id = `${path}_${filterTokens.join('_')}`; // so the collapsed state is NOT persisted between filter changes - this.contextValue = label === pathSeparator + " (" + apiTitle + ")" ? 'apiTitle' : (this.documentationUrl ? 'documentationUrl' : ''); + this.contextValue = label === pathSeparator + " (" + apiTitle + ")" ? 'apiTitle' : (this.documentationUrl ? 'documentationUrl' : ''); this.iconPath = selected ? OpenApiTreeNode.selectedSet : OpenApiTreeNode.unselectedSet; + if (clientNameOrPluginName) { + this.label = clientNameOrPluginName; + this.contextValue = 'clientNameOrPluginName'; + } } public isNodeVisible(tokenizedFilter: string[]): boolean { if (tokenizedFilter.length === 0) {