diff --git a/src/Contracts/ExplorerContracts.ts b/src/Contracts/ExplorerContracts.ts
index d1c3dba58..09a271194 100644
--- a/src/Contracts/ExplorerContracts.ts
+++ b/src/Contracts/ExplorerContracts.ts
@@ -33,6 +33,7 @@ export enum MessageTypes {
CreateWorkspace,
CreateSparkPool,
RefreshDatabaseAccount,
+ CloseTab,
}
export { Versions, ActionContracts, Diagnostics };
diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
index d9747d6ee..fdc93adea 100644
--- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
+++ b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
@@ -55,6 +55,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testAccount,
notebookServerInfo: testNotebookServerInfo,
+ tabId: undefined,
};
const wrapper = shallow();
@@ -65,6 +66,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testMongo32Account,
notebookServerInfo: testMongoNotebookServerInfo,
+ tabId: undefined,
};
const wrapper = shallow();
@@ -75,6 +77,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testMongo36Account,
notebookServerInfo: testMongoNotebookServerInfo,
+ tabId: undefined,
};
const wrapper = shallow();
@@ -85,6 +88,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testCassandraAccount,
notebookServerInfo: testCassandraNotebookServerInfo,
+ tabId: undefined,
};
const wrapper = shallow();
diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
index 637f24192..9df968226 100644
--- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
+++ b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
@@ -12,6 +12,7 @@ import * as StringUtils from "../../../Utils/StringUtils";
export interface NotebookTerminalComponentProps {
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
databaseAccount: DataModels.DatabaseAccount;
+ tabId: string;
}
export class NotebookTerminalComponent extends React.Component {
@@ -55,6 +56,7 @@ export class NotebookTerminalComponent extends React.Component tab.tabTitle() === title) as TerminalTab[];
+ .getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle().startsWith(title)) as TerminalTab[];
let index = 1;
if (terminalTabs.length > 0) {
diff --git a/src/Explorer/Tabs/TerminalTab.tsx b/src/Explorer/Tabs/TerminalTab.tsx
index 6f318f3cf..1c010d6b3 100644
--- a/src/Explorer/Tabs/TerminalTab.tsx
+++ b/src/Explorer/Tabs/TerminalTab.tsx
@@ -25,7 +25,8 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
public parameters: ko.Computed;
constructor(
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
- private getDatabaseAccount: () => DataModels.DatabaseAccount
+ private getDatabaseAccount: () => DataModels.DatabaseAccount,
+ private getTabId: () => string
) {}
public renderComponent(): JSX.Element {
@@ -33,6 +34,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
) : (
@@ -50,7 +52,8 @@ export default class TerminalTab extends TabsBase {
this.container = options.container;
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
() => this.getNotebookServerInfo(options),
- () => userContext?.databaseAccount
+ () => userContext?.databaseAccount,
+ () => this.tabId
);
this.notebookTerminalComponentAdapter.parameters = ko.computed(() => {
if (
diff --git a/src/Terminal/JupyterLabAppFactory.ts b/src/Terminal/JupyterLabAppFactory.ts
index a1eef5bbf..639469285 100644
--- a/src/Terminal/JupyterLabAppFactory.ts
+++ b/src/Terminal/JupyterLabAppFactory.ts
@@ -2,15 +2,48 @@
* JupyterLab applications based on jupyterLab components
*/
import { ServerConnection, TerminalManager } from "@jupyterlab/services";
+import { IMessage } from "@jupyterlab/services/lib/terminal/terminal";
import { Terminal } from "@jupyterlab/terminal";
import { Panel, Widget } from "@phosphor/widgets";
+import { userContext } from "UserContext";
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({
serverSettings: serverSettings,
});
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 });
if (!term) {
@@ -38,4 +71,12 @@ export class JupyterLabAppFactory {
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"));
+ }
}
diff --git a/src/Terminal/TerminalProps.ts b/src/Terminal/TerminalProps.ts
index 4fe3c539c..5122a6cb7 100644
--- a/src/Terminal/TerminalProps.ts
+++ b/src/Terminal/TerminalProps.ts
@@ -10,4 +10,5 @@ export interface TerminalProps {
authType: AuthType;
apiType: ApiType;
subscriptionId: string;
+ tabId: string;
}
diff --git a/src/Terminal/index.ts b/src/Terminal/index.ts
index dae3059b5..f71792fab 100644
--- a/src/Terminal/index.ts
+++ b/src/Terminal/index.ts
@@ -1,5 +1,6 @@
import { ServerConnection } from "@jupyterlab/services";
import "@jupyterlab/terminal/style/index.css";
+import { MessageTypes } from "Contracts/ExplorerContracts";
import postRobot from "post-robot";
import { HttpHeaders } from "../Common/Constants";
import { Action } from "../Shared/Telemetry/TelemetryConstants";
@@ -54,13 +55,20 @@ const initTerminal = async (props: TerminalProps) => {
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
try {
- await JupyterLabAppFactory.createTerminalApp(serverSettings);
+ await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings);
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
} catch (error) {
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 => {
postRobot.on(
"props",
diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts
index 319840b33..245fc1fdb 100644
--- a/src/hooks/useKnockoutExplorer.ts
+++ b/src/hooks/useKnockoutExplorer.ts
@@ -1,3 +1,4 @@
+import { useTabs } from "hooks/useTabs";
import { useEffect, useState } from "react";
import { applyExplorerBindings } from "../applyExplorerBindings";
import { AuthType } from "../AuthType";
@@ -69,16 +70,38 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
async function configureHosted(): Promise {
const win = (window as unknown) as HostedExplorerChildFrame;
+ let explorer: Explorer;
if (win.hostedConfig.authType === AuthType.EncryptedToken) {
- return configureHostedWithEncryptedToken(win.hostedConfig);
+ explorer = configureHostedWithEncryptedToken(win.hostedConfig);
} else if (win.hostedConfig.authType === AuthType.ResourceToken) {
- return configureHostedWithResourceToken(win.hostedConfig);
+ explorer = configureHostedWithResourceToken(win.hostedConfig);
} else if (win.hostedConfig.authType === AuthType.ConnectionString) {
- return configureHostedWithConnectionString(win.hostedConfig);
+ explorer = configureHostedWithConnectionString(win.hostedConfig);
} 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 {
@@ -261,6 +284,8 @@ async function configurePortal(): Promise {
}
} else if (shouldForwardMessage(message, event.origin)) {
sendMessage(message);
+ } else if (event.data?.type === MessageTypes.CloseTab) {
+ useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
}
},
false