mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-03-13 13:25:31 +00:00
Phoenix runtime - Reset workspace (#1136)
* Phoenix runtime - Reset workspace * Format and Lint issues * Typo issue * Reset warning text change and create new context on allcation of new container * Closing only notebook related * resolved comments from previous PR * On Schema Analyser allocate call Co-authored-by: Srinath Narayanan <srnara@microsoft.com>
This commit is contained in:
parent
22da3b90ef
commit
7d9faec81e
@ -344,12 +344,12 @@ export enum ConnectionStatusType {
|
|||||||
Connecting = "Connecting",
|
Connecting = "Connecting",
|
||||||
Connected = "Connected",
|
Connected = "Connected",
|
||||||
Failed = "Connection Failed",
|
Failed = "Connection Failed",
|
||||||
ReConnect = "Reconnect",
|
Reconnect = "Reconnect",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ContainerStatusType {
|
export enum ContainerStatusType {
|
||||||
Active = "Active",
|
Active = "Active",
|
||||||
InActive = "InActive",
|
Disconnected = "Disconnected",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmulatorMasterKey =
|
export const EmulatorMasterKey =
|
||||||
@ -367,8 +367,8 @@ export class Notebook {
|
|||||||
public static readonly kernelRestartMaxDelayMs = 20000;
|
public static readonly kernelRestartMaxDelayMs = 20000;
|
||||||
public static readonly autoSaveIntervalMs = 120000;
|
public static readonly autoSaveIntervalMs = 120000;
|
||||||
public static readonly memoryGuageToGB = 1048576;
|
public static readonly memoryGuageToGB = 1048576;
|
||||||
public static readonly lowMemoryBar = 0.8;
|
public static readonly lowMemoryThreshold = 0.8;
|
||||||
public static readonly reminingTimeMin = 10;
|
public static readonly remainingTimeForAlert = 10;
|
||||||
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
|
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
|
||||||
public static readonly mongoShellTemporarilyDownMsg =
|
public static readonly mongoShellTemporarilyDownMsg =
|
||||||
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
|
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
|
||||||
|
@ -430,11 +430,35 @@ export interface NotebookWorkspaceConnectionInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ContainerInfo {
|
export interface ContainerInfo {
|
||||||
durationLeftMin: number;
|
durationLeftInMinutes: number;
|
||||||
notebookServerInfo: NotebookWorkspaceConnectionInfo;
|
notebookServerInfo: NotebookWorkspaceConnectionInfo;
|
||||||
status: ContainerStatusType;
|
status: ContainerStatusType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IProvisionData {
|
||||||
|
aadToken: string;
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroup: string;
|
||||||
|
dbAccountName: string;
|
||||||
|
cosmosEndpoint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IContainerData {
|
||||||
|
dbAccountName: string;
|
||||||
|
forwardingId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResponse<T> {
|
||||||
|
status: number;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPhoenixConnectionInfoResult {
|
||||||
|
readonly notebookAuthToken?: string;
|
||||||
|
readonly notebookServerUrl?: string;
|
||||||
|
readonly forwardingId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface NotebookWorkspaceFeedResponse {
|
export interface NotebookWorkspaceFeedResponse {
|
||||||
value: NotebookWorkspace[];
|
value: NotebookWorkspace[];
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHand
|
|||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { QueriesClient } from "../Common/QueriesClient";
|
import { QueriesClient } from "../Common/QueriesClient";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import { ContainerConnectionInfo } from "../Contracts/DataModels";
|
import { ContainerConnectionInfo, IPhoenixConnectionInfoResult, IResponse } from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
@ -376,7 +376,11 @@ export default class Explorer {
|
|||||||
public async allocateContainer(): Promise<void> {
|
public async allocateContainer(): Promise<void> {
|
||||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
const isAllocating = useNotebook.getState().isAllocating;
|
const isAllocating = useNotebook.getState().isAllocating;
|
||||||
if (isAllocating === false && notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined) {
|
if (
|
||||||
|
isAllocating === false &&
|
||||||
|
(notebookServerInfo === undefined ||
|
||||||
|
(notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
|
||||||
|
) {
|
||||||
const provisionData = {
|
const provisionData = {
|
||||||
aadToken: userContext.authorizationToken,
|
aadToken: userContext.authorizationToken,
|
||||||
subscriptionId: userContext.subscriptionId,
|
subscriptionId: userContext.subscriptionId,
|
||||||
@ -391,11 +395,23 @@ export default class Explorer {
|
|||||||
try {
|
try {
|
||||||
useNotebook.getState().setIsAllocating(true);
|
useNotebook.getState().setIsAllocating(true);
|
||||||
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
|
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
|
||||||
if (
|
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||||
connectionInfo.status === HttpStatusCodes.OK &&
|
} catch (error) {
|
||||||
connectionInfo.data &&
|
connectionStatus.status = ConnectionStatusType.Failed;
|
||||||
connectionInfo.data.notebookServerUrl
|
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
this.refreshNotebookList();
|
||||||
|
|
||||||
|
this._isInitializingNotebooks = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setNotebookInfo(
|
||||||
|
connectionInfo: IResponse<IPhoenixConnectionInfoResult>,
|
||||||
|
connectionStatus: DataModels.ContainerConnectionInfo
|
||||||
) {
|
) {
|
||||||
|
if (connectionInfo.status === HttpStatusCodes.OK && connectionInfo.data && connectionInfo.data.notebookServerUrl) {
|
||||||
const containerData = {
|
const containerData = {
|
||||||
forwardingId: connectionInfo.data.forwardingId,
|
forwardingId: connectionInfo.data.forwardingId,
|
||||||
dbAccountName: userContext.databaseAccount.name,
|
dbAccountName: userContext.databaseAccount.name,
|
||||||
@ -412,20 +428,11 @@ export default class Explorer {
|
|||||||
this.notebookManager?.notebookClient
|
this.notebookManager?.notebookClient
|
||||||
.getMemoryUsage()
|
.getMemoryUsage()
|
||||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
|
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
|
||||||
useNotebook.getState().setIsAllocating(false);
|
|
||||||
} else {
|
} else {
|
||||||
connectionStatus.status = ConnectionStatusType.Failed;
|
connectionStatus.status = ConnectionStatusType.Failed;
|
||||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
useNotebook.getState().setIsAllocating(false);
|
||||||
connectionStatus.status = ConnectionStatusType.Failed;
|
|
||||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
this.refreshNotebookList();
|
|
||||||
|
|
||||||
this._isInitializingNotebooks = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public resetNotebookWorkspace(): void {
|
public resetNotebookWorkspace(): void {
|
||||||
@ -436,11 +443,14 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const dialogContent = NotebookUtil.isPhoenixEnabled()
|
||||||
|
? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?"
|
||||||
|
: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?";
|
||||||
|
|
||||||
const resetConfirmationDialogProps: DialogProps = {
|
const resetConfirmationDialogProps: DialogProps = {
|
||||||
isModal: true,
|
isModal: true,
|
||||||
title: "Reset Workspace",
|
title: "Reset Workspace",
|
||||||
subText: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?",
|
subText: dialogContent,
|
||||||
primaryButtonText: "OK",
|
primaryButtonText: "OK",
|
||||||
secondaryButtonText: "Cancel",
|
secondaryButtonText: "Cancel",
|
||||||
onPrimaryButtonClick: this._resetNotebookWorkspace,
|
onPrimaryButtonClick: this._resetNotebookWorkspace,
|
||||||
@ -498,16 +508,55 @@ export default class Explorer {
|
|||||||
private _resetNotebookWorkspace = async () => {
|
private _resetNotebookWorkspace = async () => {
|
||||||
useDialog.getState().closeDialog();
|
useDialog.getState().closeDialog();
|
||||||
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
|
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
|
||||||
|
let connectionStatus: ContainerConnectionInfo;
|
||||||
try {
|
try {
|
||||||
await this.notebookManager?.notebookClient.resetWorkspace();
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
|
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||||
|
const error = "No server endpoint detected";
|
||||||
|
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
|
||||||
|
logConsoleError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NotebookUtil.isPhoenixEnabled()) {
|
||||||
|
useTabs.getState().closeAllNotebookTabs(true);
|
||||||
|
connectionStatus = {
|
||||||
|
status: ConnectionStatusType.Connecting,
|
||||||
|
};
|
||||||
|
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||||
|
}
|
||||||
|
const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace();
|
||||||
|
if (connectionInfo && connectionInfo.status && connectionInfo.status === HttpStatusCodes.OK) {
|
||||||
|
if (NotebookUtil.isPhoenixEnabled() && connectionInfo.data && connectionInfo.data.notebookServerUrl) {
|
||||||
|
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||||
|
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||||
|
}
|
||||||
logConsoleInfo("Successfully reset notebook workspace");
|
logConsoleInfo("Successfully reset notebook workspace");
|
||||||
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
|
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
|
||||||
|
} else {
|
||||||
|
logConsoleError(`Failed to reset notebook workspace`);
|
||||||
|
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace);
|
||||||
|
if (NotebookUtil.isPhoenixEnabled()) {
|
||||||
|
connectionStatus = {
|
||||||
|
status: ConnectionStatusType.Reconnect,
|
||||||
|
};
|
||||||
|
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||||
|
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleError(`Failed to reset notebook workspace: ${error}`);
|
logConsoleError(`Failed to reset notebook workspace: ${error}`);
|
||||||
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, {
|
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, {
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
errorStack: getErrorStack(error),
|
errorStack: getErrorStack(error),
|
||||||
});
|
});
|
||||||
|
if (NotebookUtil.isPhoenixEnabled()) {
|
||||||
|
connectionStatus = {
|
||||||
|
status: ConnectionStatusType.Failed,
|
||||||
|
};
|
||||||
|
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||||
|
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearInProgressMessage();
|
clearInProgressMessage();
|
||||||
|
@ -80,9 +80,7 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
notebookButtons.push(createOpenTerminalButton(container));
|
notebookButtons.push(createOpenTerminalButton(container));
|
||||||
if (userContext.features.phoenix === false) {
|
|
||||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
(userContext.apiType === "Mongo" &&
|
(userContext.apiType === "Mongo" &&
|
||||||
useNotebook.getState().isShellEnabled &&
|
useNotebook.getState().isShellEnabled &&
|
||||||
|
@ -22,6 +22,7 @@ interface Props {
|
|||||||
container: Explorer;
|
container: Explorer;
|
||||||
}
|
}
|
||||||
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
|
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
|
||||||
|
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
||||||
const [second, setSecond] = React.useState("00");
|
const [second, setSecond] = React.useState("00");
|
||||||
const [minute, setMinute] = React.useState("00");
|
const [minute, setMinute] = React.useState("00");
|
||||||
const [isActive, setIsActive] = React.useState(false);
|
const [isActive, setIsActive] = React.useState(false);
|
||||||
@ -67,6 +68,12 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
|
|||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [isActive, counter]);
|
}, [isActive, counter]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Reconnect) {
|
||||||
|
setToolTipContent("Click here to Reconnect to temporary workspace.");
|
||||||
|
}
|
||||||
|
}, [connectionInfo.status]);
|
||||||
|
|
||||||
const stopTimer = () => {
|
const stopTimer = () => {
|
||||||
setIsActive(false);
|
setIsActive(false);
|
||||||
setCounter(0);
|
setCounter(0);
|
||||||
@ -74,15 +81,13 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
|
|||||||
setMinute("00");
|
setMinute("00");
|
||||||
};
|
};
|
||||||
|
|
||||||
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
|
||||||
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
|
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
|
||||||
|
|
||||||
const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0;
|
const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0;
|
||||||
const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0;
|
const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
connectionInfo &&
|
connectionInfo &&
|
||||||
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.ReConnect)
|
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect)
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
|
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
|
||||||
@ -113,11 +118,10 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
|
|||||||
<>
|
<>
|
||||||
<TooltipHost
|
<TooltipHost
|
||||||
content={
|
content={
|
||||||
containerInfo.status &&
|
containerInfo?.status === ContainerStatusType.Active &&
|
||||||
containerInfo.status === ContainerStatusType.Active &&
|
Math.round(containerInfo.durationLeftInMinutes) <= Notebook.remainingTimeForAlert
|
||||||
containerInfo.durationLeftMin <= Notebook.reminingTimeMin
|
? `Connected to temporary workspace. This temporary workspace will get disconnected in ${Math.round(
|
||||||
? `Connected to temporary workspace. This temporary workspace will get deleted in ${Math.round(
|
containerInfo.durationLeftInMinutes
|
||||||
containerInfo.durationLeftMin
|
|
||||||
)} minutes.`
|
)} minutes.`
|
||||||
: toolTipContent
|
: toolTipContent
|
||||||
}
|
}
|
||||||
@ -139,16 +143,16 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
|
|||||||
)}
|
)}
|
||||||
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
|
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
|
||||||
<ProgressIndicator
|
<ProgressIndicator
|
||||||
className={usedGB / totalGB > Notebook.lowMemoryBar ? "lowMemory" : ""}
|
className={totalGB !== 0 && usedGB / totalGB > 0.8 ? "lowMemory" : ""}
|
||||||
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
|
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
|
||||||
percentComplete={usedGB / totalGB}
|
percentComplete={totalGB !== 0 ? usedGB / totalGB : 0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
{!isBarDismissed &&
|
{!isBarDismissed &&
|
||||||
containerInfo.status &&
|
containerInfo.status &&
|
||||||
containerInfo.status === ContainerStatusType.Active &&
|
containerInfo.status === ContainerStatusType.Active &&
|
||||||
containerInfo.durationLeftMin <= Notebook.reminingTimeMin ? (
|
Math.round(containerInfo.durationLeftInMinutes) <= Notebook.remainingTimeForAlert ? (
|
||||||
<FocusTrapCallout
|
<FocusTrapCallout
|
||||||
role="alertdialog"
|
role="alertdialog"
|
||||||
className={styles.callout}
|
className={styles.callout}
|
||||||
@ -158,16 +162,16 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
|
|||||||
setInitialFocus
|
setInitialFocus
|
||||||
>
|
>
|
||||||
<Text block variant="xLarge" className={styles.title}>
|
<Text block variant="xLarge" className={styles.title}>
|
||||||
Remaining time
|
Remaining Time
|
||||||
</Text>
|
</Text>
|
||||||
<Text block variant="small">
|
<Text block variant="small">
|
||||||
This temporary workspace will get deleted in {Math.round(containerInfo.durationLeftMin)} minutes. To
|
This temporary workspace will get disconnected in {Math.round(containerInfo.durationLeftInMinutes)}{" "}
|
||||||
save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your
|
minutes. To save your work permanently, save your notebooks to a GitHub repository or download the
|
||||||
local machine before the session ends.
|
notebooks to your local machine before the session ends.
|
||||||
</Text>
|
</Text>
|
||||||
<FocusZone handleTabKey={FocusZoneTabbableElements.all} isCircularNavigation>
|
<FocusZone handleTabKey={FocusZoneTabbableElements.all} isCircularNavigation>
|
||||||
<Stack className={styles.buttons} gap={8} horizontal>
|
<Stack className={styles.buttons} gap={8} horizontal>
|
||||||
<DefaultButton onClick={() => setIsBarDismissed(true)}>Dismiss</DefaultButton>
|
<DefaultButton onClick={() => setIsBarDismissed(true)}>Dimiss</DefaultButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
</FocusZone>
|
</FocusZone>
|
||||||
</FocusTrapCallout>
|
</FocusTrapCallout>
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* Notebook container related stuff
|
* Notebook container related stuff
|
||||||
*/
|
*/
|
||||||
|
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { ConnectionStatusType } from "../../Common/Constants";
|
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes } from "../../Common/Constants";
|
||||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
|
import {
|
||||||
|
ContainerConnectionInfo,
|
||||||
|
IPhoenixConnectionInfoResult,
|
||||||
|
IProvisionData,
|
||||||
|
IResponse,
|
||||||
|
} from "../../Contracts/DataModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||||
|
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { NotebookUtil } from "./NotebookUtil";
|
import { NotebookUtil } from "./NotebookUtil";
|
||||||
import { useNotebook } from "./useNotebook";
|
import { useNotebook } from "./useNotebook";
|
||||||
@ -103,12 +110,9 @@ export class NotebookContainerClient {
|
|||||||
|
|
||||||
private checkStatus(): boolean {
|
private checkStatus(): boolean {
|
||||||
if (NotebookUtil.isPhoenixEnabled()) {
|
if (NotebookUtil.isPhoenixEnabled()) {
|
||||||
if (
|
if (useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Disconnected) {
|
||||||
useNotebook.getState().containerStatus?.status &&
|
|
||||||
useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.InActive
|
|
||||||
) {
|
|
||||||
const connectionStatus: ContainerConnectionInfo = {
|
const connectionStatus: ContainerConnectionInfo = {
|
||||||
status: ConnectionStatusType.ReConnect,
|
status: ConnectionStatusType.Reconnect,
|
||||||
};
|
};
|
||||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||||
@ -117,17 +121,21 @@ export class NotebookContainerClient {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
public async resetWorkspace(): Promise<void> {
|
|
||||||
|
public async resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> {
|
||||||
this.isResettingWorkspace = true;
|
this.isResettingWorkspace = true;
|
||||||
|
let response: IResponse<IPhoenixConnectionInfoResult>;
|
||||||
try {
|
try {
|
||||||
await this._resetWorkspace();
|
response = await this._resetWorkspace();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Promise.reject(error);
|
Promise.reject(error);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
this.isResettingWorkspace = false;
|
this.isResettingWorkspace = false;
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _resetWorkspace(): Promise<void> {
|
private async _resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> {
|
||||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||||
const error = "No server endpoint detected";
|
const error = "No server endpoint detected";
|
||||||
@ -137,14 +145,41 @@ export class NotebookContainerClient {
|
|||||||
|
|
||||||
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
||||||
try {
|
try {
|
||||||
await fetch(`${notebookServerEndpoint}/api/shutdown`, {
|
let data: IPhoenixConnectionInfoResult;
|
||||||
|
let response: Response;
|
||||||
|
if (NotebookUtil.isPhoenixEnabled()) {
|
||||||
|
const provisionData: IProvisionData = {
|
||||||
|
aadToken: userContext.authorizationToken,
|
||||||
|
subscriptionId: userContext.subscriptionId,
|
||||||
|
resourceGroup: userContext.resourceGroup,
|
||||||
|
dbAccountName: userContext.databaseAccount.name,
|
||||||
|
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||||
|
};
|
||||||
|
response = await fetch(`${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer/reset`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: this.getHeaders(),
|
||||||
|
body: JSON.stringify(provisionData),
|
||||||
|
});
|
||||||
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
|
data = await response.json();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response = await fetch(`${notebookServerEndpoint}/api/shutdown`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { Authorization: authToken },
|
headers: { Authorization: authToken },
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
data,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
|
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
|
||||||
|
if (!NotebookUtil.isPhoenixEnabled()) {
|
||||||
await this.recreateNotebookWorkspaceAsync();
|
await this.recreateNotebookWorkspaceAsync();
|
||||||
}
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNotebookServerConfig(): { notebookServerEndpoint: string; authToken: string } {
|
private getNotebookServerConfig(): { notebookServerEndpoint: string; authToken: string } {
|
||||||
@ -175,4 +210,12 @@ export class NotebookContainerClient {
|
|||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getHeaders(): HeadersInit {
|
||||||
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
|
return {
|
||||||
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
|
[HttpHeaders.contentType]: "application/json",
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import * as Logger from "../../Common/Logger";
|
|||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
|
import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
|
||||||
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
@ -88,7 +89,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
isRefreshed: false,
|
isRefreshed: false,
|
||||||
containerStatus: {
|
containerStatus: {
|
||||||
status: undefined,
|
status: undefined,
|
||||||
durationLeftMin: undefined,
|
durationLeftInMinutes: undefined,
|
||||||
notebookServerInfo: undefined,
|
notebookServerInfo: undefined,
|
||||||
},
|
},
|
||||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||||
@ -279,16 +280,13 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
|
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
|
||||||
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
|
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
|
||||||
resetContainerConnection: (connectionStatus: ContainerConnectionInfo): void => {
|
resetContainerConnection: (connectionStatus: ContainerConnectionInfo): void => {
|
||||||
|
useTabs.getState().closeAllNotebookTabs(true);
|
||||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||||
useNotebook.getState().setNotebookServerInfo({
|
useNotebook.getState().setNotebookServerInfo(undefined);
|
||||||
notebookServerEndpoint: undefined,
|
|
||||||
authToken: undefined,
|
|
||||||
forwardingId: undefined,
|
|
||||||
});
|
|
||||||
useNotebook.getState().setIsAllocating(false);
|
useNotebook.getState().setIsAllocating(false);
|
||||||
useNotebook.getState().setContainerStatus({
|
useNotebook.getState().setContainerStatus({
|
||||||
status: undefined,
|
status: undefined,
|
||||||
durationLeftMin: undefined,
|
durationLeftInMinutes: undefined,
|
||||||
notebookServerInfo: undefined,
|
notebookServerInfo: undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -17,6 +17,7 @@ export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Every notebook-based tab inherits from this class. It holds the static reference to a notebook client (singleton)
|
* Every notebook-based tab inherits from this class. It holds the static reference to a notebook client (singleton)
|
||||||
|
* Re-initiating the constructor when ever a new container got allocated.
|
||||||
*/
|
*/
|
||||||
export default class NotebookTabBase extends TabsBase {
|
export default class NotebookTabBase extends TabsBase {
|
||||||
protected static clientManager: NotebookClientV2;
|
protected static clientManager: NotebookClientV2;
|
||||||
@ -27,6 +28,15 @@ export default class NotebookTabBase extends TabsBase {
|
|||||||
|
|
||||||
this.container = options.container;
|
this.container = options.container;
|
||||||
|
|
||||||
|
useNotebook.subscribe(
|
||||||
|
() => {
|
||||||
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
|
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint) {
|
||||||
|
NotebookTabBase.clientManager = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(state) => state.notebookServerInfo
|
||||||
|
);
|
||||||
if (!NotebookTabBase.clientManager) {
|
if (!NotebookTabBase.clientManager) {
|
||||||
NotebookTabBase.clientManager = new NotebookClientV2({
|
NotebookTabBase.clientManager = new NotebookClientV2({
|
||||||
connectionInfo: useNotebook.getState().notebookServerInfo,
|
connectionInfo: useNotebook.getState().notebookServerInfo,
|
||||||
|
@ -53,14 +53,16 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
|||||||
onUpdateKernelInfo: this.onKernelUpdate,
|
onUpdateKernelInfo: this.onKernelUpdate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
public onCloseTabButtonClick(): Q.Promise<any> {
|
* Hard cleaning the workspace(Closing tabs connected with old container connection) when new container got allocated.
|
||||||
|
*/
|
||||||
|
public onCloseTabButtonClick(hardClose = false): Q.Promise<any> {
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
this.notebookComponentAdapter.notebookShutdown();
|
this.notebookComponentAdapter.notebookShutdown();
|
||||||
super.onCloseTabButtonClick();
|
super.onCloseTabButtonClick();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.notebookComponentAdapter.isContentDirty()) {
|
if (this.notebookComponentAdapter.isContentDirty() && hardClose === false) {
|
||||||
useDialog
|
useDialog
|
||||||
.getState()
|
.getState()
|
||||||
.showOkCancelModalDialog(
|
.showOkCancelModalDialog(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||||
|
import { NotebookUtil } from "Explorer/Notebook/NotebookUtil";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@ -528,6 +529,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onSchemaAnalyzerClick = async () => {
|
public onSchemaAnalyzerClick = async () => {
|
||||||
|
if (NotebookUtil.isPhoenixEnabled()) {
|
||||||
|
await this.container.allocateContainer();
|
||||||
|
}
|
||||||
useSelectedNode.getState().setSelectedNode(this);
|
useSelectedNode.getState().setSelectedNode(this);
|
||||||
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
|
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
|
||||||
const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default;
|
const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default;
|
||||||
|
@ -2,37 +2,21 @@ import { ContainerStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../
|
|||||||
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import { ContainerInfo } from "../Contracts/DataModels";
|
import {
|
||||||
|
ContainerInfo,
|
||||||
|
IContainerData,
|
||||||
|
IPhoenixConnectionInfoResult,
|
||||||
|
IProvisionData,
|
||||||
|
IResponse,
|
||||||
|
} from "../Contracts/DataModels";
|
||||||
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||||
|
|
||||||
export interface IPhoenixResponse<T> {
|
|
||||||
status: number;
|
|
||||||
data: T;
|
|
||||||
}
|
|
||||||
export interface IPhoenixConnectionInfoResult {
|
|
||||||
readonly notebookAuthToken?: string;
|
|
||||||
readonly notebookServerUrl?: string;
|
|
||||||
readonly forwardingId?: string;
|
|
||||||
}
|
|
||||||
export interface IProvisionData {
|
|
||||||
cosmosEndpoint: string;
|
|
||||||
dbAccountName: string;
|
|
||||||
aadToken: string;
|
|
||||||
resourceGroup: string;
|
|
||||||
subscriptionId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IContainerData {
|
|
||||||
dbAccountName: string;
|
|
||||||
forwardingId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PhoenixClient {
|
export class PhoenixClient {
|
||||||
public async containerConnectionInfo(
|
public async containerConnectionInfo(
|
||||||
provisionData: IProvisionData
|
provisionData: IProvisionData
|
||||||
): Promise<IPhoenixResponse<IPhoenixConnectionInfoResult>> {
|
): Promise<IResponse<IPhoenixConnectionInfoResult>> {
|
||||||
try {
|
try {
|
||||||
const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/allocate`, {
|
const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/allocate`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -52,6 +36,7 @@ export class PhoenixClient {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initiateContainerHeartBeat(containerData: { forwardingId: string; dbAccountName: string }) {
|
public async initiateContainerHeartBeat(containerData: { forwardingId: string; dbAccountName: string }) {
|
||||||
this.getContainerHealth(Notebook.containerStatusHeartbeatDelayMs, containerData);
|
this.getContainerHealth(Notebook.containerStatusHeartbeatDelayMs, containerData);
|
||||||
}
|
}
|
||||||
@ -74,22 +59,22 @@ export class PhoenixClient {
|
|||||||
if (response.status === HttpStatusCodes.OK) {
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
const containerStatus = await response.json();
|
const containerStatus = await response.json();
|
||||||
return {
|
return {
|
||||||
durationLeftMin: containerStatus.durationLeftInMinutes,
|
durationLeftInMinutes: containerStatus?.durationLeftInMinutes,
|
||||||
notebookServerInfo: containerStatus.notebookServerInfo,
|
notebookServerInfo: containerStatus?.notebookServerInfo,
|
||||||
status: ContainerStatusType.Active,
|
status: ContainerStatusType.Active,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
durationLeftMin: undefined,
|
durationLeftInMinutes: undefined,
|
||||||
notebookServerInfo: undefined,
|
notebookServerInfo: undefined,
|
||||||
status: ContainerStatusType.InActive,
|
status: ContainerStatusType.Disconnected,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.logError(getErrorMessage(error), "PhoenixClient/getContainerStatus");
|
Logger.logError(getErrorMessage(error), "PhoenixClient/getContainerStatus");
|
||||||
return {
|
return {
|
||||||
durationLeftMin: undefined,
|
durationLeftInMinutes: undefined,
|
||||||
notebookServerInfo: undefined,
|
notebookServerInfo: undefined,
|
||||||
status: ContainerStatusType.InActive,
|
status: ContainerStatusType.Disconnected,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,10 +83,7 @@ export class PhoenixClient {
|
|||||||
this.getContainerStatusAsync(containerData)
|
this.getContainerStatusAsync(containerData)
|
||||||
.then((ContainerInfo) => useNotebook.getState().setContainerStatus(ContainerInfo))
|
.then((ContainerInfo) => useNotebook.getState().setContainerStatus(ContainerInfo))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (
|
if (useNotebook.getState().containerStatus?.status === ContainerStatusType.Active) {
|
||||||
useNotebook.getState().containerStatus.status &&
|
|
||||||
useNotebook.getState().containerStatus.status === ContainerStatusType.Active
|
|
||||||
) {
|
|
||||||
this.scheduleContainerHeartbeat(delayMs, containerData);
|
this.scheduleContainerHeartbeat(delayMs, containerData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
import { CollectionTabKind } from "../Contracts/ViewModels";
|
||||||
|
import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab";
|
||||||
import TabsBase from "../Explorer/Tabs/TabsBase";
|
import TabsBase from "../Explorer/Tabs/TabsBase";
|
||||||
|
|
||||||
interface TabsState {
|
interface TabsState {
|
||||||
@ -12,6 +14,7 @@ interface TabsState {
|
|||||||
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void;
|
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void;
|
||||||
closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void;
|
closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void;
|
||||||
closeTab: (tab: TabsBase) => void;
|
closeTab: (tab: TabsBase) => void;
|
||||||
|
closeAllNotebookTabs: (hardClose: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||||
@ -78,4 +81,31 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
|||||||
|
|
||||||
set({ openedTabs: updatedTabs });
|
set({ openedTabs: updatedTabs });
|
||||||
},
|
},
|
||||||
|
closeAllNotebookTabs: (hardClose): void => {
|
||||||
|
const isNotebook = (tabKind: CollectionTabKind): boolean => {
|
||||||
|
if (
|
||||||
|
tabKind === CollectionTabKind.Notebook ||
|
||||||
|
tabKind === CollectionTabKind.NotebookV2 ||
|
||||||
|
tabKind === CollectionTabKind.SchemaAnalyzer ||
|
||||||
|
tabKind === CollectionTabKind.Terminal
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabList = get().openedTabs;
|
||||||
|
if (tabList && tabList.length > 0) {
|
||||||
|
tabList.forEach((tab: NotebookTabV2) => {
|
||||||
|
const tabKind: CollectionTabKind = tab.tabKind;
|
||||||
|
if (tabKind && isNotebook(tabKind)) {
|
||||||
|
tab.onCloseTabButtonClick(hardClose);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (get().openedTabs.length === 0) {
|
||||||
|
set({ activeTab: undefined });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user