Close mongo and casssandra terminal tabs once the shells are exited (#1183)

* initial commit for closing terminal

* added extra case

* lint changes and hostee explorer fixes

* fixed lint errors

* fixed compile error

* fixed review comments
This commit is contained in:
Srinath Narayanan 2022-01-10 11:58:35 -08:00 committed by GitHub
parent 591782195d
commit b765cae088
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 95 additions and 10 deletions

View File

@ -33,6 +33,7 @@ export enum MessageTypes {
CreateWorkspace, CreateWorkspace,
CreateSparkPool, CreateSparkPool,
RefreshDatabaseAccount, RefreshDatabaseAccount,
CloseTab,
} }
export { Versions, ActionContracts, Diagnostics }; export { Versions, ActionContracts, Diagnostics };

View File

@ -55,6 +55,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = { const props: NotebookTerminalComponentProps = {
databaseAccount: testAccount, databaseAccount: testAccount,
notebookServerInfo: testNotebookServerInfo, notebookServerInfo: testNotebookServerInfo,
tabId: undefined,
}; };
const wrapper = shallow(<NotebookTerminalComponent {...props} />); const wrapper = shallow(<NotebookTerminalComponent {...props} />);
@ -65,6 +66,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = { const props: NotebookTerminalComponentProps = {
databaseAccount: testMongo32Account, databaseAccount: testMongo32Account,
notebookServerInfo: testMongoNotebookServerInfo, notebookServerInfo: testMongoNotebookServerInfo,
tabId: undefined,
}; };
const wrapper = shallow(<NotebookTerminalComponent {...props} />); const wrapper = shallow(<NotebookTerminalComponent {...props} />);
@ -75,6 +77,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = { const props: NotebookTerminalComponentProps = {
databaseAccount: testMongo36Account, databaseAccount: testMongo36Account,
notebookServerInfo: testMongoNotebookServerInfo, notebookServerInfo: testMongoNotebookServerInfo,
tabId: undefined,
}; };
const wrapper = shallow(<NotebookTerminalComponent {...props} />); const wrapper = shallow(<NotebookTerminalComponent {...props} />);
@ -85,6 +88,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = { const props: NotebookTerminalComponentProps = {
databaseAccount: testCassandraAccount, databaseAccount: testCassandraAccount,
notebookServerInfo: testCassandraNotebookServerInfo, notebookServerInfo: testCassandraNotebookServerInfo,
tabId: undefined,
}; };
const wrapper = shallow(<NotebookTerminalComponent {...props} />); const wrapper = shallow(<NotebookTerminalComponent {...props} />);

View File

@ -12,6 +12,7 @@ import * as StringUtils from "../../../Utils/StringUtils";
export interface NotebookTerminalComponentProps { export interface NotebookTerminalComponentProps {
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
databaseAccount: DataModels.DatabaseAccount; databaseAccount: DataModels.DatabaseAccount;
tabId: string;
} }
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> { export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
@ -55,6 +56,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
apiType: userContext.apiType, apiType: userContext.apiType,
authType: userContext.authType, authType: userContext.authType,
databaseAccount: userContext.databaseAccount, databaseAccount: userContext.databaseAccount,
tabId: this.props.tabId,
}; };
postRobot.send(this.terminalWindow, "props", props, { postRobot.send(this.terminalWindow, "props", props, {

View File

@ -1096,7 +1096,7 @@ export default class Explorer {
const terminalTabs: TerminalTab[] = useTabs const terminalTabs: TerminalTab[] = useTabs
.getState() .getState()
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle() === title) as TerminalTab[]; .getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle().startsWith(title)) as TerminalTab[];
let index = 1; let index = 1;
if (terminalTabs.length > 0) { if (terminalTabs.length > 0) {

View File

@ -25,7 +25,8 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
public parameters: ko.Computed<boolean>; public parameters: ko.Computed<boolean>;
constructor( constructor(
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo, private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
private getDatabaseAccount: () => DataModels.DatabaseAccount private getDatabaseAccount: () => DataModels.DatabaseAccount,
private getTabId: () => string
) {} ) {}
public renderComponent(): JSX.Element { public renderComponent(): JSX.Element {
@ -33,6 +34,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
<NotebookTerminalComponent <NotebookTerminalComponent
notebookServerInfo={this.getNotebookServerInfo()} notebookServerInfo={this.getNotebookServerInfo()}
databaseAccount={this.getDatabaseAccount()} databaseAccount={this.getDatabaseAccount()}
tabId={this.getTabId()}
/> />
) : ( ) : (
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner> <Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
@ -50,7 +52,8 @@ export default class TerminalTab extends TabsBase {
this.container = options.container; this.container = options.container;
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter( this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
() => this.getNotebookServerInfo(options), () => this.getNotebookServerInfo(options),
() => userContext?.databaseAccount () => userContext?.databaseAccount,
() => this.tabId
); );
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => { this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
if ( if (

View File

@ -2,15 +2,48 @@
* JupyterLab applications based on jupyterLab components * JupyterLab applications based on jupyterLab components
*/ */
import { ServerConnection, TerminalManager } from "@jupyterlab/services"; import { ServerConnection, TerminalManager } from "@jupyterlab/services";
import { IMessage } from "@jupyterlab/services/lib/terminal/terminal";
import { Terminal } from "@jupyterlab/terminal"; import { Terminal } from "@jupyterlab/terminal";
import { Panel, Widget } from "@phosphor/widgets"; import { Panel, Widget } from "@phosphor/widgets";
import { userContext } from "UserContext";
export class JupyterLabAppFactory { export class JupyterLabAppFactory {
public static async createTerminalApp(serverSettings: ServerConnection.ISettings) { private isShellClosed: boolean;
private onShellExited: () => void;
private checkShellClosed: ((content: string | undefined) => boolean | undefined) | undefined;
constructor(closeTab: () => void) {
this.onShellExited = closeTab;
this.isShellClosed = false;
this.checkShellClosed = undefined;
switch (userContext.apiType) {
case "Mongo":
this.checkShellClosed = JupyterLabAppFactory.isMongoShellClosed;
break;
case "Cassandra":
this.checkShellClosed = JupyterLabAppFactory.isCassandraShellClosed;
break;
}
}
public async createTerminalApp(serverSettings: ServerConnection.ISettings) {
const manager = new TerminalManager({ const manager = new TerminalManager({
serverSettings: serverSettings, serverSettings: serverSettings,
}); });
const session = await manager.startNew(); const session = await manager.startNew();
session.messageReceived.connect(async (_, message: IMessage) => {
const content = message.content && message.content[0]?.toString();
if (this.checkShellClosed && message.type == "stdout") {
//Close the terminal tab once the shell closed messages are received
if (this.checkShellClosed(content)) {
this.isShellClosed = true;
} else if (content?.includes("cosmosuser@") && this.isShellClosed) {
this.onShellExited();
}
}
}, this);
const term = new Terminal(session, { theme: "dark", shutdownOnClose: true }); const term = new Terminal(session, { theme: "dark", shutdownOnClose: true });
if (!term) { if (!term) {
@ -38,4 +71,12 @@ export class JupyterLabAppFactory {
panel.dispose(); panel.dispose();
}); });
} }
private static isMongoShellClosed(content: string | undefined) {
return content?.endsWith("bye\r\n") || (content?.includes("Stopped") && content?.includes("mongo --host"));
}
private static isCassandraShellClosed(content: string | undefined) {
return content == "\r\n" || (content?.includes("Stopped") && content?.includes("cqlsh"));
}
} }

View File

@ -10,4 +10,5 @@ export interface TerminalProps {
authType: AuthType; authType: AuthType;
apiType: ApiType; apiType: ApiType;
subscriptionId: string; subscriptionId: string;
tabId: string;
} }

View File

@ -1,5 +1,6 @@
import { ServerConnection } from "@jupyterlab/services"; import { ServerConnection } from "@jupyterlab/services";
import "@jupyterlab/terminal/style/index.css"; import "@jupyterlab/terminal/style/index.css";
import { MessageTypes } from "Contracts/ExplorerContracts";
import postRobot from "post-robot"; import postRobot from "post-robot";
import { HttpHeaders } from "../Common/Constants"; import { HttpHeaders } from "../Common/Constants";
import { Action } from "../Shared/Telemetry/TelemetryConstants"; import { Action } from "../Shared/Telemetry/TelemetryConstants";
@ -54,13 +55,20 @@ const initTerminal = async (props: TerminalProps) => {
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data); const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
try { try {
await JupyterLabAppFactory.createTerminalApp(serverSettings); await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings);
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime); TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
} catch (error) { } catch (error) {
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime); TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
} }
}; };
const closeTab = (tabId: string): void => {
window.parent.postMessage(
{ type: MessageTypes.CloseTab, data: { tabId: tabId }, signature: "pcIframe" },
window.document.referrer
);
};
const main = async (): Promise<void> => { const main = async (): Promise<void> => {
postRobot.on( postRobot.on(
"props", "props",

View File

@ -1,3 +1,4 @@
import { useTabs } from "hooks/useTabs";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { applyExplorerBindings } from "../applyExplorerBindings"; import { applyExplorerBindings } from "../applyExplorerBindings";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
@ -69,16 +70,38 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
async function configureHosted(): Promise<Explorer> { async function configureHosted(): Promise<Explorer> {
const win = (window as unknown) as HostedExplorerChildFrame; const win = (window as unknown) as HostedExplorerChildFrame;
let explorer: Explorer;
if (win.hostedConfig.authType === AuthType.EncryptedToken) { if (win.hostedConfig.authType === AuthType.EncryptedToken) {
return configureHostedWithEncryptedToken(win.hostedConfig); explorer = configureHostedWithEncryptedToken(win.hostedConfig);
} else if (win.hostedConfig.authType === AuthType.ResourceToken) { } else if (win.hostedConfig.authType === AuthType.ResourceToken) {
return configureHostedWithResourceToken(win.hostedConfig); explorer = configureHostedWithResourceToken(win.hostedConfig);
} else if (win.hostedConfig.authType === AuthType.ConnectionString) { } else if (win.hostedConfig.authType === AuthType.ConnectionString) {
return configureHostedWithConnectionString(win.hostedConfig); explorer = configureHostedWithConnectionString(win.hostedConfig);
} else if (win.hostedConfig.authType === AuthType.AAD) { } else if (win.hostedConfig.authType === AuthType.AAD) {
return configureHostedWithAAD(win.hostedConfig); explorer = await configureHostedWithAAD(win.hostedConfig);
} } else {
throw new Error(`Unknown hosted config: ${win.hostedConfig}`); throw new Error(`Unknown hosted config: ${win.hostedConfig}`);
}
window.addEventListener(
"message",
(event) => {
if (isInvalidParentFrameOrigin(event)) {
return;
}
if (!shouldProcessMessage(event)) {
return;
}
if (event.data?.type === MessageTypes.CloseTab) {
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
}
},
false
);
return explorer;
} }
async function configureHostedWithAAD(config: AAD): Promise<Explorer> { async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
@ -261,6 +284,8 @@ async function configurePortal(): Promise<Explorer> {
} }
} else if (shouldForwardMessage(message, event.origin)) { } else if (shouldForwardMessage(message, event.origin)) {
sendMessage(message); sendMessage(message);
} else if (event.data?.type === MessageTypes.CloseTab) {
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
} }
}, },
false false