diff --git a/package.json b/package.json index cb0106e21..9c96f4cf0 100644 --- a/package.json +++ b/package.json @@ -915,6 +915,16 @@ "command": "openshift.Serverless.invoke", "title": "Invoke", "category": "OpenShift" + }, + { + "command": "openshift.Serverless.showInTerminal", + "title": "Show in terminal", + "category": "OpenShift" + }, + { + "command": "openshift.Serverless.removeSession", + "title": "Remove Serverless Session", + "category": "OpenShift" } ], "keybindings": [ @@ -1349,6 +1359,14 @@ { "command": "openshift.Serverless.removeGit", "when": "false" + }, + { + "command": "openshift.Serverless.showInTerminal", + "when": "false" + }, + { + "command": "openshift.Serverless.removeSession", + "when": "false" } ], "view/title": [ diff --git a/src/serverlessFunction/functions.ts b/src/serverlessFunction/functions.ts index 991a03571..2393713c8 100644 --- a/src/serverlessFunction/functions.ts +++ b/src/serverlessFunction/functions.ts @@ -9,14 +9,13 @@ import { CliChannel } from '../cli'; import { Oc } from '../oc/ocWrapper'; import { Odo } from '../odo/odoWrapper'; import { isTektonAware } from '../tekton/tekton'; -import { Platform } from '../util/platform'; import { Progress } from '../util/progress'; import { OpenShiftTerminalApi, OpenShiftTerminalManager } from '../webview/openshift-terminal/openShiftTerminal'; import { ServerlessCommand, Utils } from './commands'; import { GitModel, getGitBranchInteractively, getGitRepoInteractively, getGitStateByPath } from './git/git'; import { isKnativeServingAware } from './knative'; import { multiStep } from './multiStepInput'; -import { FunctionContent, FunctionObject, InvokeFunction } from './types'; +import { FunctionContent, FunctionObject, FunctionSession } from './types'; export class Functions { @@ -114,7 +113,7 @@ export class Functions { private async clustrBuildTerminal(context: FunctionObject, namespace: string, buildImage: string, gitModel: GitModel) { const isOpenShiftCluster = await Oc.Instance.isOpenShiftCluster(); - await OpenShiftTerminalManager.getInstance().createTerminal( + const terminal = await OpenShiftTerminalManager.getInstance().createTerminal( ServerlessCommand.onClusterBuildFunction(context.folderURI.fsPath, namespace, buildImage, gitModel, isOpenShiftCluster), `On Cluster Build: ${context.name}`, context.folderURI.fsPath, @@ -122,11 +121,28 @@ export class Functions { onExit: undefined, } , true ); + const session = { + sessionName: `On Cluster Build: ${context.name}`, + sessionPath: context.folderURI, + teminal: terminal + }; + this.addSession(context, session); + void commands.executeCommand('openshift.Serverless.refresh', context); + } + + private addSession(context: FunctionObject, session: FunctionSession) { + if (context.sessions?.length > 0) { + const withoutExistingSameSession = context.sessions.filter((exSession) => exSession.sessionName !== session.sessionName); + context.sessions = withoutExistingSameSession; + context.sessions.push(session); + } else { + context.sessions = []; + context.sessions.push(session); + } } public async build(context: FunctionObject, s2iBuild: boolean): Promise { const existingTerminal: OpenShiftTerminalApi = this.buildTerminalMap.get(`build-${context.folderURI.fsPath}`); - if (existingTerminal) { void window.showWarningMessage(`Do you want to restart ${context.name} build ?`, 'Yes', 'No').then(async (value: string) => { if (value === 'Yes') { @@ -152,7 +168,7 @@ export class Functions { const isOpenShiftCluster = await Oc.Instance.isOpenShiftCluster(); const buildImage = await this.getImage(context.folderURI); const terminalKey = `build-${context.folderURI.fsPath}`; - await this.buildTerminal(context, s2iBuild ? 's2i' : 'pack',buildImage, isOpenShiftCluster, terminalKey); + await this.buildTerminal(context, s2iBuild ? 's2i' : 'pack', buildImage, isOpenShiftCluster, terminalKey); } private async buildTerminal(context: FunctionObject, builder: string, buildImage: string, isOpenShiftCluster: boolean, terminalKey: string) { @@ -164,9 +180,17 @@ export class Functions { { onExit: () => { this.buildTerminalMap.delete(terminalKey); - } + }, + }, true ); + const session: FunctionSession = { + sessionName: `Build: ${context.name}`, + sessionPath: context.folderURI, + teminal: terminal + } + this.addSession(context, session); + void commands.executeCommand('openshift.Serverless.refresh', context); this.buildTerminalMap.set(terminalKey, terminal); } @@ -186,6 +210,13 @@ export class Functions { } }, true ); + const session: FunctionSession = { + sessionName: `${runBuild ? 'Build and ' : ''}Run: ${context.name}`, + sessionPath: context.folderURI, + teminal: terminal + } + this.addSession(context, session); + void commands.executeCommand('openshift.Serverless.refresh', context); this.runTerminalMap.set(`run-${context.folderURI.fsPath}`, terminal); } @@ -207,14 +238,6 @@ export class Functions { }); } - public async getTemplates(): Promise { - const result = await Odo.Instance.execute(ServerlessCommand.getTemplates(), undefined, false); - if (result.error) { - void window.showErrorMessage(result.error.message); - } - return JSON.parse(result.stdout) as any[]; - } - public async deploy(context: FunctionObject) { const currentNamespace: string = await Odo.Instance.getActiveProject(); const yamlContent = await Utils.getFuncYamlContent(context.folderURI.fsPath); @@ -269,16 +292,14 @@ export class Functions { }, }, true ); - } - public async invoke(functionName: string, invokeFunData: InvokeFunction): Promise { - await OpenShiftTerminalManager.getInstance().createTerminal( - ServerlessCommand.invokeFunction(invokeFunData), - `Invoke: ${functionName}`, - undefined, undefined, { - onExit: undefined - }, true - ); + const session = { + sessionName: `Deploy: ${context.name}`, + sessionPath: context.folderURI, + teminal: terminal + }; + this.addSession(context, session); + void commands.executeCommand('openshift.Serverless.refresh', context); } public async config(title: string, context: FunctionObject, mode: string, isAdd = true) { @@ -336,16 +357,6 @@ export class Functions { }); } - public getDefaultImages(name: string): string[] { - const imageList: string[] = []; - const defaultUsername = Platform.getEnv(); - const defaultQuayImage = `quay.io/${Platform.getOS() === 'win32' ? defaultUsername.USERNAME : defaultUsername.USER}/${name}:latest`; - const defaultDockerImage = `docker.io/${Platform.getOS() === 'win32' ? defaultUsername.USERNAME : defaultUsername.USER}/${name}:latest`; - imageList.push(defaultQuayImage); - imageList.push(defaultDockerImage); - return imageList; - } - public async getImage(folderURI: Uri): Promise { const yamlContent = await Utils.getFuncYamlContent(folderURI.fsPath); if (yamlContent?.image && Functions.imageRegex.test(yamlContent.image)) { diff --git a/src/serverlessFunction/types.ts b/src/serverlessFunction/types.ts index de673ae51..954b5441f 100644 --- a/src/serverlessFunction/types.ts +++ b/src/serverlessFunction/types.ts @@ -4,6 +4,7 @@ *-----------------------------------------------------------------------------------------------*/ import { Uri } from 'vscode'; +import { OpenShiftTerminalApi } from '../webview/openshift-terminal/openShiftTerminal'; export interface FunctionView { refresh(context?: FunctionObject); @@ -33,6 +34,14 @@ export interface FunctionObject { hasImage?: boolean; hadBuilt?: boolean; isRunning?: boolean; + sessions?: FunctionSession[]; +} + +export interface FunctionSession { + sessionName: string; + sessionPath: Uri; + teminal?: OpenShiftTerminalApi; + isDone?: boolean; } export interface GitModel { diff --git a/src/serverlessFunction/view.ts b/src/serverlessFunction/view.ts index c6ed3939a..03475afe1 100644 --- a/src/serverlessFunction/view.ts +++ b/src/serverlessFunction/view.ts @@ -16,6 +16,7 @@ import { TreeItemCollapsibleState, TreeView, Uri, + commands, window, workspace } from 'vscode'; @@ -24,9 +25,9 @@ import ServerlessFunctionViewLoader from '../webview/serverless-function/serverl import ManageRepositoryViewLoader from '../webview/serverless-manage-repository/manageRepositoryLoader'; import { ServerlessFunctionModel } from './functionModel'; import { Functions } from './functions'; -import { FunctionContextType, FunctionObject, FunctionStatus } from './types'; +import { FunctionContextType, FunctionObject, FunctionSession, FunctionStatus } from './types'; -type ExplorerItem = KubernetesObject | FunctionObject | Context | TreeItem; +type ExplorerItem = KubernetesObject | FunctionObject | FunctionSession | Context | TreeItem; export class ServerlessFunctionView implements TreeDataProvider, Disposable { private static instance: ServerlessFunctionView; @@ -41,6 +42,8 @@ export class ServerlessFunctionView implements TreeDataProvider, D private serverlessFunction: ServerlessFunctionModel; + private serverlessFunctionTreeNodes: Map = new Map(); + private constructor() { this.serverlessFunction = new ServerlessFunctionModel(this); this.treeView = window.createTreeView('openshiftServerlessFunctionsView', { @@ -56,7 +59,6 @@ export class ServerlessFunctionView implements TreeDataProvider, D } getTreeItem(element: ExplorerItem): TreeItem | Thenable { - if ('kind' in element) { if (element.kind === 'project') { return { @@ -69,18 +71,27 @@ export class ServerlessFunctionView implements TreeDataProvider, D const functionObj: FunctionObject = element; const explorerItem: ExplorerItem = { label: functionObj?.name, - collapsibleState: TreeItemCollapsibleState.None + collapsibleState: element.sessions?.length > 0 ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None } if (functionObj.context !== FunctionStatus.NONE) { - explorerItem.iconPath = new ThemeIcon('symbol-function'), + explorerItem.iconPath = new ThemeIcon('symbol-function'), explorerItem.description = this.getDescription(functionObj.context), explorerItem.tooltip = this.getTooltip(functionObj), explorerItem.contextValue = this.getContext(functionObj), explorerItem.command = this.getCommand(functionObj); } return explorerItem; + } else if ('sessionName' in element) { + const functionSession: FunctionSession = element; + const explorerItem: ExplorerItem = { + label: element.sessionName.substring(0, element.sessionName.indexOf(':')).trim(), + collapsibleState: TreeItemCollapsibleState.None, + iconPath: new ThemeIcon('symbol-value'), + } + explorerItem.contextValue = 'FunctionSession'; + explorerItem.command = this.getSessionCommand(functionSession) + return explorerItem; } - } getContext(functionObj: FunctionObject): string { @@ -104,6 +115,10 @@ export class ServerlessFunctionView implements TreeDataProvider, D return { command: 'openshift.Serverless.openFunction', title: 'Open Function', arguments: [functionObj] }; } + getSessionCommand(functionSession: FunctionSession): Command { + return { command: 'openshift.Serverless.showInTerminal', title: 'Show in terminal', arguments: [functionSession] }; + } + getTooltip(functionObj: FunctionObject): string { let text = ''; if (functionObj.name) { @@ -128,13 +143,15 @@ export class ServerlessFunctionView implements TreeDataProvider, D let result: ExplorerItem[] = []; if (!element) { result = [...await this.serverlessFunction.getLocalFunctions()] - if (result.length === 0) { - const functionNode: FunctionObject = { - name: 'No Available Functions', - context: FunctionStatus.NONE - } - result = [functionNode] + if (result.length === 0) { + const functionNode: FunctionObject = { + name: 'No Available Functions', + context: FunctionStatus.NONE } + result = [functionNode] + } + } else if ('sessions' in element && element.sessions.length > 0) { + result = [...this.processSessions(element)]; } return result; } @@ -226,6 +243,15 @@ export class ServerlessFunctionView implements TreeDataProvider, D ); } + @vsCommand('openshift.Serverless.showInTerminal') + static async openTerminal(context: FunctionSession) { + if (!context || context.isDone) { + return null; + } + await commands.executeCommand('openShiftTerminalView.focus'); + context.teminal.focusTerminal(); + } + @vsCommand('openshift.Serverless.addEnv') static async addEnv(context: FunctionObject) { await Functions.getInstance().config(`Add environment variables '${context.name}'`, context, 'envs'); @@ -265,4 +291,26 @@ export class ServerlessFunctionView implements TreeDataProvider, D static async removeGit(context: FunctionObject) { await Functions.getInstance().config(`Remove Git '${context.name}'`, context, 'git', false); } + + @vsCommand('openshift.Serverless.removeSession') + static removeSesson(uuid: string ,cwdPath: string, sessionName: string) { + if (ServerlessFunctionView.getInstance().serverlessFunctionTreeNodes.has(cwdPath)) { + const element = ServerlessFunctionView.getInstance().serverlessFunctionTreeNodes.get(cwdPath); + const index = element.sessions.findIndex((session) => session.sessionName === sessionName && session.teminal.id === uuid); + if(index !== -1) { + element.sessions.splice(index, 1); + ServerlessFunctionView.getInstance().refresh(element); + } + } + } + + processSessions(element: FunctionObject): FunctionSession[] { + const functionSessions: FunctionSession[] = []; + this.serverlessFunctionTreeNodes.set(element.folderURI.fsPath, element); + const treeNode = this.serverlessFunctionTreeNodes.get(element.folderURI.fsPath); + treeNode.sessions.forEach((session: FunctionSession) => { + functionSessions.push(session); + }); + return functionSessions; + } } diff --git a/src/webview/openshift-terminal/openShiftTerminal.ts b/src/webview/openshift-terminal/openShiftTerminal.ts index b28fac95a..93192dd53 100644 --- a/src/webview/openshift-terminal/openShiftTerminal.ts +++ b/src/webview/openshift-terminal/openShiftTerminal.ts @@ -24,6 +24,7 @@ import { CliChannel } from '../../cli'; import { ToolsConfig } from '../../tools'; import { getVscodeModule } from '../../util/credentialManager'; import { loadWebviewHtml } from '../common-ext/utils'; +import { Platform } from '../../util/platform'; // HACK: we cannot include node-pty ourselves, // since the library can only be run under one version of node @@ -65,6 +66,8 @@ export interface OpenShiftTerminalApi { * Close the terminal. If the extension is not running on Windows, the process will be terminated using SIGABRT. */ forceKill: () => void; + + id: string; } /** @@ -202,6 +205,24 @@ class OpenShiftTerminal { this._ptyExited = true; } + /** + * Returns the exe file of the execution. + * + * @returns the exe file of the execution. + */ + public get file() { + return this._file; + } + + /** + * Returns the current working directory of the execution. + * + * @returns the current working directory. + */ + public get cwd(): string { + return this._options?.cwd; + } + /** * Returns the name of this terminal. * @@ -238,6 +259,10 @@ class OpenShiftTerminal { return this._pty && !this._ptyExited; } + public get isPtyExit() { + return this._ptyExited; + } + /** * Returns the string data of the terminal, serialized */ @@ -461,6 +486,13 @@ export class OpenShiftTerminalManager implements WebviewViewProvider { } else if (message.kind === 'resize') { terminal.resize(message.data.cols, message.data.rows); } else if (message.kind === 'closeTerminal') { + let serverlessFuncTool = 'func'; + if (Platform.OS === 'win32') { + serverlessFuncTool = serverlessFuncTool.concat('.exe'); + } + if (terminal.file.endsWith(serverlessFuncTool)) { + void commands.executeCommand('openshift.Serverless.removeSession' , terminal.uuid, terminal.cwd, terminal.name); + } terminal.dispose(); this.openShiftTerminals.delete(message?.data?.uuid); } else if (message.kind === 'openExternal') { @@ -645,6 +677,7 @@ export class OpenShiftTerminalManager implements WebviewViewProvider { void this.sendMessage({ kind: 'switchToTerminal', data: { uuid: newTermUUID } }), kill: () => this.openShiftTerminals.get(newTermUUID).write('\u0003'), forceKill: () => this.openShiftTerminals.get(newTermUUID).forceKill(), + id: newTermUUID }; } diff --git a/src/webview/serverless-function/serverlessFunctionLoader.ts b/src/webview/serverless-function/serverlessFunctionLoader.ts index 2b8306560..efd79e5f4 100644 --- a/src/webview/serverless-function/serverlessFunctionLoader.ts +++ b/src/webview/serverless-function/serverlessFunctionLoader.ts @@ -10,7 +10,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { Odo } from '../../odo/odoWrapper'; import { ServerlessCommand, Utils } from '../../serverlessFunction/commands'; -import { Functions } from '../../serverlessFunction/functions'; + import { InvokeFunction } from '../../serverlessFunction/types'; import { CliExitData } from '../../util/childProcessUtil'; import { ExtensionID } from '../../util/constants'; @@ -18,12 +18,42 @@ import { Progress } from '../../util/progress'; import { selectWorkspaceFolder, selectWorkspaceFolders } from '../../util/workspace'; import { VsCommandError } from '../../vscommand'; import { loadWebviewHtml, validateName } from '../common-ext/utils'; +import { Platform } from '../../util/platform'; +import { OpenShiftTerminalManager } from '../openshift-terminal/openShiftTerminal'; export interface ServiceBindingFormResponse { selectedService: string; bindingName: string; } +function getDefaultImages(name: string): string[] { + const imageList: string[] = []; + const defaultUsername = Platform.getEnv(); + const defaultQuayImage = `quay.io/${Platform.getOS() === 'win32' ? defaultUsername.USERNAME : defaultUsername.USER}/${name}:latest`; + const defaultDockerImage = `docker.io/${Platform.getOS() === 'win32' ? defaultUsername.USERNAME : defaultUsername.USER}/${name}:latest`; + imageList.push(defaultQuayImage); + imageList.push(defaultDockerImage); + return imageList; +} + +async function getTemplates(): Promise { + const result = await Odo.Instance.execute(ServerlessCommand.getTemplates(), undefined, false); + if (result.error) { + void vscode.window.showErrorMessage(result.error.message); + } + return JSON.parse(result.stdout) as any[]; +} + +async function invoke(functionName: string, invokeFunData: InvokeFunction): Promise { + await OpenShiftTerminalManager.getInstance().createTerminal( + ServerlessCommand.invokeFunction(invokeFunData), + `Invoke: ${functionName}`, + undefined, undefined, { + onExit: undefined + }, true + ); + } + async function messageListener(panel: vscode.WebviewPanel, event: any): Promise { let response: CliExitData; const eventName = event.action; @@ -33,7 +63,7 @@ async function messageListener(panel: vscode.WebviewPanel, event: any): Promise< case 'validateName': { const flag = validateName(functionName); const defaultImages = !flag - ? Functions.getInstance().getDefaultImages(functionName) + ? getDefaultImages(functionName) : []; void panel?.webview.postMessage({ action: eventName, @@ -117,7 +147,7 @@ async function messageListener(panel: vscode.WebviewPanel, event: any): Promise< enableURL: event.enableURL, invokeURL: event.invokeURL, }; - await Functions.getInstance().invoke(functionName, invokeFunData); + await invoke(functionName, invokeFunData); panel.dispose(); break; } @@ -155,7 +185,7 @@ export default class ServerlessFunctionViewLoader { panel.reveal(vscode.ViewColumn.One); return null; } - const templates = await Functions.getInstance().getTemplates(); + const templates = await getTemplates(); if (invoke) { const panel = await this.createView(title); const getEnvFuncId = crypto.randomUUID();