mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-02 07:40:45 +00:00
Compare commits
1 Commits
fix_a11y_D
...
users/dech
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f1b0b01ba |
@@ -1,4 +1,3 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
|
||||
"isTerminalEnabled" : true
|
||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com"
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||
"isTerminalEnabled" : false
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com"
|
||||
}
|
||||
|
||||
@@ -2885,10 +2885,6 @@ a:link {
|
||||
|
||||
.settingsSectionPart {
|
||||
padding-left: 8px;
|
||||
label {
|
||||
padding: 0 !important;
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.settingsSectionLabel {
|
||||
@@ -2899,14 +2895,6 @@ a:link {
|
||||
.pageOptionsPart {
|
||||
padding-bottom: @MediumSpace;
|
||||
}
|
||||
|
||||
.legendLabel {
|
||||
border-bottom: 0px;
|
||||
width: auto;
|
||||
font-size: @mediumFontSize;
|
||||
display: inline !important;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove these styles once we refactor all buttons to use the command button component
|
||||
|
||||
29670
package-lock.json
generated
29670
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -96,8 +96,7 @@ export class Flights {
|
||||
public static readonly AutoscaleTest = "autoscaletest";
|
||||
public static readonly PartitionKeyTest = "partitionkeytest";
|
||||
public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
|
||||
public static readonly PhoenixNotebooks = "phoenixnotebooks";
|
||||
public static readonly PhoenixFeatures = "phoenixfeatures";
|
||||
public static readonly Phoenix = "phoenix";
|
||||
public static readonly NotebooksDownBanner = "notebooksdownbanner";
|
||||
}
|
||||
|
||||
@@ -366,7 +365,7 @@ export class Notebook {
|
||||
public static readonly containerStatusHeartbeatDelayMs = 30000;
|
||||
public static readonly kernelRestartInitialDelayMs = 1000;
|
||||
public static readonly kernelRestartMaxDelayMs = 20000;
|
||||
public static readonly autoSaveIntervalMs = 300000;
|
||||
public static readonly autoSaveIntervalMs = 120000;
|
||||
public static readonly memoryGuageToGB = 1048576;
|
||||
public static readonly lowMemoryThreshold = 0.8;
|
||||
public static readonly remainingTimeForAlert = 10;
|
||||
@@ -379,7 +378,7 @@ export class Notebook {
|
||||
"We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation.";
|
||||
public static saveNotebookModalTitle = "Save notebook in temporary workspace";
|
||||
public static saveNotebookModalContent =
|
||||
"This notebook will be saved in the temporary workspace and will be removed when the session expires.";
|
||||
"This notebook will be saved in the temporary workspace and will be removed when the session expires. To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends.";
|
||||
public static newNotebookModalTitle = "Create notebook in temporary workspace";
|
||||
public static newNotebookUploadModalTitle = "Upload notebook to temporary workspace";
|
||||
public static newNotebookModalContent1 =
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as Cosmos from "@azure/cosmos";
|
||||
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
|
||||
import { CosmosHeaders } from "@azure/cosmos/dist-esm";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
@@ -78,21 +77,10 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
|
||||
}
|
||||
}
|
||||
|
||||
// The Capability is a bitmap, which cosmosdb backend decodes as per the below enum
|
||||
enum SDKSupportedCapabilities {
|
||||
None = 0,
|
||||
PartitionMerge = 1 << 0,
|
||||
}
|
||||
|
||||
let _client: Cosmos.CosmosClient;
|
||||
|
||||
export function client(): Cosmos.CosmosClient {
|
||||
if (_client) return _client;
|
||||
|
||||
let _defaultHeaders: CosmosHeaders = {};
|
||||
_defaultHeaders["x-ms-cosmos-sdk-supported-capabilities"] =
|
||||
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
|
||||
|
||||
const options: Cosmos.CosmosClientOptions = {
|
||||
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
|
||||
key: userContext.masterKey,
|
||||
@@ -101,7 +89,6 @@ export function client(): Cosmos.CosmosClient {
|
||||
enableEndpointDiscovery: false,
|
||||
},
|
||||
userAgentSuffix: "Azure Portal",
|
||||
defaultHeaders: _defaultHeaders,
|
||||
};
|
||||
|
||||
if (configContext.PROXY_PATH !== undefined) {
|
||||
|
||||
@@ -27,7 +27,6 @@ export interface ConfigContext {
|
||||
GITHUB_CLIENT_ID: string;
|
||||
GITHUB_TEST_ENV_CLIENT_ID: string;
|
||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||
isTerminalEnabled: boolean;
|
||||
hostedExplorerURL: string;
|
||||
armAPIVersion?: string;
|
||||
allowedJunoOrigins: string[];
|
||||
@@ -60,7 +59,6 @@ let configContext: Readonly<ConfigContext> = {
|
||||
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
|
||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
isTerminalEnabled: false,
|
||||
allowedJunoOrigins: [
|
||||
JunoEndpoints.Test,
|
||||
JunoEndpoints.Test2,
|
||||
|
||||
@@ -438,10 +438,14 @@ export interface ContainerInfo {
|
||||
}
|
||||
|
||||
export interface IProvisionData {
|
||||
subscriptionId: string;
|
||||
resourceGroup: string;
|
||||
dbAccountName: string;
|
||||
cosmosEndpoint: string;
|
||||
}
|
||||
|
||||
export interface IContainerData {
|
||||
dbAccountName: string;
|
||||
forwardingId: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ export enum MessageTypes {
|
||||
CreateWorkspace,
|
||||
CreateSparkPool,
|
||||
RefreshDatabaseAccount,
|
||||
CloseTab,
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
|
||||
@@ -83,6 +83,7 @@ export const createCollectionContextMenuButton = (
|
||||
|
||||
items.push({
|
||||
iconSrc: HostedTerminalIcon,
|
||||
isDisabled: useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown,
|
||||
onClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
if (useNotebook.getState().isShellEnabled) {
|
||||
|
||||
@@ -55,7 +55,6 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testAccount,
|
||||
notebookServerInfo: testNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@@ -66,7 +65,6 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testMongo32Account,
|
||||
notebookServerInfo: testMongoNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@@ -77,7 +75,6 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testMongo36Account,
|
||||
notebookServerInfo: testMongoNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
@@ -88,7 +85,6 @@ describe("NotebookTerminalComponent", () => {
|
||||
const props: NotebookTerminalComponentProps = {
|
||||
databaseAccount: testCassandraAccount,
|
||||
notebookServerInfo: testCassandraNotebookServerInfo,
|
||||
tabId: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
||||
|
||||
@@ -12,7 +12,6 @@ import * as StringUtils from "../../../Utils/StringUtils";
|
||||
export interface NotebookTerminalComponentProps {
|
||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||
databaseAccount: DataModels.DatabaseAccount;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
|
||||
@@ -56,7 +55,6 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
apiType: userContext.apiType,
|
||||
authType: userContext.authType,
|
||||
databaseAccount: userContext.databaseAccount,
|
||||
tabId: this.props.tabId,
|
||||
};
|
||||
|
||||
postRobot.send(this.terminalWindow, "props", props, {
|
||||
|
||||
@@ -17,6 +17,7 @@ import Explorer from "../../Explorer";
|
||||
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
||||
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
||||
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
||||
import { NotebookUtil } from "../../Notebook/NotebookUtil";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { Dialog, TextFieldProps, useDialog } from "../Dialog";
|
||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||
@@ -147,7 +148,9 @@ export class NotebookViewerComponent
|
||||
<NotebookMetadataComponent
|
||||
data={this.state.galleryItem}
|
||||
isFavorite={this.state.isFavorite}
|
||||
downloadButtonText={this.props.container && `Download to ${useNotebook.getState().notebookFolderName}`}
|
||||
downloadButtonText={
|
||||
this.props.container && NotebookUtil.getNotebookBtnTitle(useNotebook.getState().notebookFolderName)
|
||||
}
|
||||
onTagClick={this.props.onTagClick}
|
||||
onFavoriteClick={this.favoriteItem}
|
||||
onUnfavoriteClick={this.unfavoriteItem}
|
||||
|
||||
@@ -14,12 +14,7 @@ import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHand
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { QueriesClient } from "../Common/QueriesClient";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import {
|
||||
ContainerConnectionInfo,
|
||||
IPhoenixConnectionInfoResult,
|
||||
IProvisionData,
|
||||
IResponse,
|
||||
} from "../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo, IPhoenixConnectionInfoResult, IResponse } from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
@@ -35,6 +30,7 @@ import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import {
|
||||
get as getWorkspace,
|
||||
listByDatabaseAccount,
|
||||
listConnectionInfo,
|
||||
start,
|
||||
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
@@ -356,7 +352,24 @@ export default class Explorer {
|
||||
return;
|
||||
}
|
||||
this._isInitializingNotebooks = true;
|
||||
if (userContext.features.phoenix === false) {
|
||||
await this.ensureNotebookWorkspaceRunning();
|
||||
const connectionInfo = await listConnectionInfo(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
|
||||
forwardingId: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
this.refreshNotebookList();
|
||||
|
||||
this._isInitializingNotebooks = false;
|
||||
}
|
||||
|
||||
@@ -368,7 +381,10 @@ export default class Explorer {
|
||||
(notebookServerInfo === undefined ||
|
||||
(notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
|
||||
) {
|
||||
const provisionData: IProvisionData = {
|
||||
const provisionData = {
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
dbAccountName: userContext.databaseAccount.name,
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
};
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
@@ -439,7 +455,7 @@ export default class Explorer {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const dialogContent = useNotebook.getState().isPhoenixNotebooks
|
||||
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?";
|
||||
|
||||
@@ -516,7 +532,7 @@ export default class Explorer {
|
||||
TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
useTabs.getState().closeAllNotebookTabs(true);
|
||||
connectionStatus = {
|
||||
status: ConnectionStatusType.Connecting,
|
||||
@@ -530,7 +546,7 @@ export default class Explorer {
|
||||
if (!connectionInfo?.data?.notebookServerUrl) {
|
||||
throw new Error(`Reset Workspace: NotebookServerUrl is invalid!`);
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
await this.setNotebookInfo(connectionInfo, connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
}
|
||||
@@ -545,7 +561,7 @@ export default class Explorer {
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
});
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
connectionStatus = {
|
||||
status: ConnectionStatusType.Failed,
|
||||
};
|
||||
@@ -743,7 +759,7 @@ export default class Explorer {
|
||||
if (!notebookContentItem || !notebookContentItem.path) {
|
||||
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
|
||||
}
|
||||
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (notebookContentItem.type === NotebookContentItemType.Notebook && NotebookUtil.isPhoenixEnabled()) {
|
||||
await this.allocateContainer();
|
||||
}
|
||||
|
||||
@@ -961,17 +977,20 @@ export default class Explorer {
|
||||
/**
|
||||
* This creates a new notebook file, then opens the notebook
|
||||
*/
|
||||
public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
|
||||
public onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): void {
|
||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to create new notebook, but notebook is not enabled";
|
||||
handleError(error, "Explorer/onNewNotebookClicked");
|
||||
throw new Error(error);
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
const isPhoenixEnabled = NotebookUtil.isPhoenixEnabled();
|
||||
if (isPhoenixEnabled) {
|
||||
if (isGithubTree) {
|
||||
await this.allocateContainer();
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.createNewNoteBook(parent, isGithubTree);
|
||||
async () => {
|
||||
await this.allocateContainer();
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.createNewNoteBook(parent, isGithubTree);
|
||||
};
|
||||
} else {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
Notebook.newNotebookModalTitle,
|
||||
@@ -1056,7 +1075,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
|
||||
if (useNotebook.getState().isPhoenixFeatures) {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
await this.allocateContainer();
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
|
||||
@@ -1096,7 +1115,7 @@ export default class Explorer {
|
||||
|
||||
const terminalTabs: TerminalTab[] = useTabs
|
||||
.getState()
|
||||
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle().startsWith(title)) as TerminalTab[];
|
||||
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle() === title) as TerminalTab[];
|
||||
|
||||
let index = 1;
|
||||
if (terminalTabs.length > 0) {
|
||||
@@ -1197,7 +1216,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public async handleOpenFileAction(path: string): Promise<void> {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (userContext.features.phoenix) {
|
||||
await this.allocateContainer();
|
||||
} else if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
|
||||
this._openSetupNotebooksPaneForQuickstart();
|
||||
@@ -1231,7 +1250,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public openUploadFilePanel(parent?: NotebookContentItem): void {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
Notebook.newNotebookUploadModalTitle,
|
||||
undefined,
|
||||
@@ -1261,7 +1280,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public getDownloadModalConent(fileName: string): JSX.Element {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
return (
|
||||
<>
|
||||
<p>{Notebook.galleryNotebookDownloadContent1}</p>
|
||||
@@ -1285,19 +1304,22 @@ export default class Explorer {
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
|
||||
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||
const isNotebookEnabled = userContext.features.notebooksDownBanner || useNotebook.getState().isPhoenixNotebooks;
|
||||
const isNotebookEnabled = userContext.features.notebooksDownBanner || userContext.features.phoenix;
|
||||
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
||||
useNotebook
|
||||
.getState()
|
||||
.setIsShellEnabled(useNotebook.getState().isPhoenixFeatures && isPublicInternetAccessAllowed());
|
||||
useNotebook.getState().setIsShellEnabled(userContext.features.phoenix && isPublicInternetAccessAllowed());
|
||||
|
||||
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
|
||||
isNotebookEnabled,
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
});
|
||||
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.initNotebooks(userContext.databaseAccount);
|
||||
if (!userContext.features.notebooksTemporarilyDown) {
|
||||
if (isNotebookEnabled) {
|
||||
await this.initNotebooks(userContext.databaseAccount);
|
||||
} else if (this.notebookToImport) {
|
||||
// if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane
|
||||
this._openSetupNotebooksPaneForQuickstart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import * as React from "react";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { NotebookUtil } from "../../Notebook/NotebookUtil";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
||||
import * as CommandBarUtil from "./CommandBarUtil";
|
||||
@@ -53,10 +56,18 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||
|
||||
if (useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
|
||||
}
|
||||
|
||||
if (
|
||||
userContext.features.phoenix === false &&
|
||||
userContext.features.notebooksTemporarilyDown === false &&
|
||||
useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2
|
||||
) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="commandBarContainer">
|
||||
<FluentCommandBar
|
||||
|
||||
@@ -31,13 +31,28 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Button should be visible", () => {
|
||||
it("Account is not serverless - button should be visible", () => {
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Account is serverless - button should be hidden", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableServerless" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Enable notebook button", () => {
|
||||
|
||||
@@ -25,6 +25,7 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { JunoClient } from "../../../Juno/JunoClient";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
|
||||
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
|
||||
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
@@ -77,10 +78,9 @@ export function createStaticCommandBarButtons(
|
||||
if (container.notebookManager?.gitHubOAuthService) {
|
||||
notebookButtons.push(createManageGitHubAccountButton(container));
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
|
||||
notebookButtons.push(createOpenTerminalButton(container));
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
|
||||
|
||||
notebookButtons.push(createOpenTerminalButton(container));
|
||||
if (selectedNodeState.isConnectedToContainer()) {
|
||||
notebookButtons.push(createNotebookWorkspaceResetButton(container));
|
||||
}
|
||||
if (
|
||||
@@ -98,21 +98,19 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
|
||||
notebookButtons.forEach((btn) => {
|
||||
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
|
||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||
if (userContext.features.notebooksTemporarilyDown) {
|
||||
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
|
||||
}
|
||||
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
|
||||
if (!useNotebook.getState().isPhoenixFeatures) {
|
||||
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
|
||||
} else {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
||||
}
|
||||
} else if (!useNotebook.getState().isPhoenixNotebooks) {
|
||||
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
|
||||
}
|
||||
buttons.push(btn);
|
||||
});
|
||||
} else {
|
||||
if (!isRunningOnNationalCloud() && useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (!isRunningOnNationalCloud() && !userContext.features.notebooksTemporarilyDown) {
|
||||
buttons.push(createDivider());
|
||||
buttons.push(createEnableNotebooksButton(container));
|
||||
}
|
||||
@@ -170,7 +168,9 @@ export function createContextCommandBarButtons(
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
|
||||
if (useNotebook.getState().isShellEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
if (!userContext.features.notebooksTemporarilyDown) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
}
|
||||
} else {
|
||||
selectedCollection && selectedCollection.onNewMongoShellClick();
|
||||
}
|
||||
@@ -178,6 +178,13 @@ export function createContextCommandBarButtons(
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
tooltipText:
|
||||
useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown
|
||||
? Constants.Notebook.mongoShellTemporarilyDownMsg
|
||||
: undefined,
|
||||
disabled:
|
||||
(selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") ||
|
||||
(useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown),
|
||||
};
|
||||
buttons.push(newMongoShellBtn);
|
||||
}
|
||||
@@ -273,6 +280,10 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isServerlessAccount()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Link } from "@fluentui/react";
|
||||
import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable";
|
||||
// Vendor modules
|
||||
import {
|
||||
@@ -15,15 +14,13 @@ import "@nteract/styles/editor-overrides.css";
|
||||
import "@nteract/styles/global-variables.css";
|
||||
import "codemirror/addon/hint/show-hint.css";
|
||||
import "codemirror/lib/codemirror.css";
|
||||
import { Notebook } from "Common/Constants";
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import * as Immutable from "immutable";
|
||||
import * as React from "react";
|
||||
import { Provider } from "react-redux";
|
||||
import "react-table/react-table.css";
|
||||
import { AnyAction, Store } from "redux";
|
||||
import { NotebookClientV2 } from "../NotebookClientV2";
|
||||
import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
import * as NteractUtil from "../NTeractUtil";
|
||||
import * as CdbActions from "./actions";
|
||||
import { NotebookComponent } from "./NotebookComponent";
|
||||
@@ -102,10 +99,6 @@ export class NotebookComponentBootstrapper {
|
||||
};
|
||||
}
|
||||
|
||||
public getNotebookPath(): string {
|
||||
return this.getStore().getState().core.entities.contents.byRef.get(this.contentRef)?.filepath;
|
||||
}
|
||||
|
||||
public setContent(name: string, content: unknown): void {
|
||||
this.getStore().dispatch(
|
||||
actions.fetchContentFulfilled({
|
||||
@@ -137,32 +130,11 @@ export class NotebookComponentBootstrapper {
|
||||
|
||||
/* Notebook operations. See nteract/packages/connected-components/src/notebook-menu/index.tsx */
|
||||
public notebookSave(): void {
|
||||
if (
|
||||
NotebookUtil.getContentProviderType(this.getNotebookPath()) ===
|
||||
NotebookContentProviderType.JupyterContentProviderType
|
||||
) {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
Notebook.saveNotebookModalTitle,
|
||||
undefined,
|
||||
"Save",
|
||||
async () => {
|
||||
this.getStore().dispatch(
|
||||
actions.save({
|
||||
contentRef: this.contentRef,
|
||||
})
|
||||
);
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
this.getSaveNotebookSubText()
|
||||
);
|
||||
} else {
|
||||
this.getStore().dispatch(
|
||||
actions.save({
|
||||
contentRef: this.contentRef,
|
||||
})
|
||||
);
|
||||
}
|
||||
this.getStore().dispatch(
|
||||
actions.save({
|
||||
contentRef: this.contentRef,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public notebookChangeKernel(kernelSpecName: string): void {
|
||||
@@ -369,19 +341,4 @@ export class NotebookComponentBootstrapper {
|
||||
protected getStore(): Store<AppState, AnyAction> {
|
||||
return this.notebookClient.getStore();
|
||||
}
|
||||
|
||||
private getSaveNotebookSubText(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<p>{Notebook.saveNotebookModalContent}</p>
|
||||
<br />
|
||||
<p>
|
||||
{Notebook.newNotebookModalContent2}
|
||||
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
|
||||
{Notebook.learnMore}
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,11 @@ import {
|
||||
ServerConfig as JupyterServerConfig,
|
||||
} from "@nteract/core";
|
||||
import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging";
|
||||
import { defineConfigOption } from "@nteract/mythic-configuration";
|
||||
import { RecordOf } from "immutable";
|
||||
import { Action, AnyAction } from "redux";
|
||||
import { AnyAction } from "redux";
|
||||
import { ofType, StateObservable } from "redux-observable";
|
||||
import { kernels, sessions } from "rx-jupyter";
|
||||
import { concat, EMPTY, from, interval, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs";
|
||||
import { concat, EMPTY, from, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs";
|
||||
import {
|
||||
catchError,
|
||||
concatMap,
|
||||
@@ -42,7 +41,7 @@ import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationCons
|
||||
import { useDialog } from "../../Controls/Dialog";
|
||||
import * as FileSystemUtil from "../FileSystemUtil";
|
||||
import * as cdbActions from "../NotebookComponent/actions";
|
||||
import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
|
||||
import { NotebookUtil } from "../NotebookUtil";
|
||||
import * as CdbActions from "./actions";
|
||||
import * as TextFile from "./contents/file/text-file";
|
||||
import { CdbAppState } from "./types";
|
||||
@@ -949,54 +948,6 @@ const resetCellStatusOnExecuteCanceledEpic = (
|
||||
);
|
||||
};
|
||||
|
||||
const { selector: autoSaveInterval } = defineConfigOption({
|
||||
key: "autoSaveInterval",
|
||||
label: "Auto-save interval",
|
||||
defaultValue: 120_000,
|
||||
});
|
||||
|
||||
/**
|
||||
* Override autoSaveCurrentContentEpic to disable auto save for notebooks under temporary workspace.
|
||||
* @param action$
|
||||
*/
|
||||
export function autoSaveCurrentContentEpic(
|
||||
action$: Observable<Action>,
|
||||
state$: StateObservable<AppState>
|
||||
): Observable<actions.Save> {
|
||||
return state$.pipe(
|
||||
map((state) => autoSaveInterval(state)),
|
||||
switchMap((time) => interval(time)),
|
||||
mergeMap(() => {
|
||||
const state = state$.value;
|
||||
return from(
|
||||
selectors
|
||||
.contentByRef(state)
|
||||
.filter(
|
||||
/*
|
||||
* Only save contents that are files or notebooks with
|
||||
* a filepath already set.
|
||||
*/
|
||||
(content) => (content.type === "file" || content.type === "notebook") && content.filepath !== ""
|
||||
)
|
||||
.keys()
|
||||
);
|
||||
}),
|
||||
filter((contentRef: ContentRef) => {
|
||||
const model = selectors.model(state$.value, { contentRef });
|
||||
const content = selectors.content(state$.value, { contentRef });
|
||||
if (
|
||||
model &&
|
||||
model.type === "notebook" &&
|
||||
NotebookUtil.getContentProviderType(content.filepath) !== NotebookContentProviderType.JupyterContentProviderType
|
||||
) {
|
||||
return selectors.notebook.isDirty(model);
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
map((contentRef: ContentRef) => actions.save({ contentRef }))
|
||||
);
|
||||
}
|
||||
|
||||
export const allEpics = [
|
||||
addInitialCodeCellEpic,
|
||||
focusInitialCodeCellEpic,
|
||||
@@ -1014,5 +965,4 @@ export const allEpics = [
|
||||
traceNotebookInfoEpic,
|
||||
traceNotebookKernelEpic,
|
||||
resetCellStatusOnExecuteCanceledEpic,
|
||||
autoSaveCurrentContentEpic,
|
||||
];
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { AppState, epics as coreEpics, IContentProvider, reducers } from "@nteract/core";
|
||||
import { AppState, epics as coreEpics, reducers, IContentProvider } from "@nteract/core";
|
||||
import { compose, Store, AnyAction, Middleware, Dispatch, MiddlewareAPI } from "redux";
|
||||
import { Epic } from "redux-observable";
|
||||
import { allEpics } from "./epics";
|
||||
import { coreReducer, cdbReducer } from "./reducers";
|
||||
import { catchError } from "rxjs/operators";
|
||||
import { Observable } from "rxjs";
|
||||
import { configuration } from "@nteract/mythic-configuration";
|
||||
import { makeConfigureStore } from "@nteract/myths";
|
||||
import { AnyAction, compose, Dispatch, Middleware, MiddlewareAPI, Store } from "redux";
|
||||
import { Epic } from "redux-observable";
|
||||
import { Observable } from "rxjs";
|
||||
import { catchError } from "rxjs/operators";
|
||||
import { allEpics } from "./epics";
|
||||
import { cdbReducer, coreReducer } from "./reducers";
|
||||
import { CdbAppState } from "./types";
|
||||
|
||||
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
@@ -81,6 +81,7 @@ export const getCoreEpics = (autoStartKernelOnNotebookOpen: boolean): Epic[] =>
|
||||
// This list needs to be consistent and in sync with core.allEpics until we figure
|
||||
// out how to safely filter out the ones we are overriding here.
|
||||
const filteredCoreEpics = [
|
||||
coreEpics.autoSaveCurrentContentEpic,
|
||||
coreEpics.executeCellEpic,
|
||||
coreEpics.executeFocusedCellEpic,
|
||||
coreEpics.executeCellAfterKernelLaunchEpic,
|
||||
|
||||
@@ -8,10 +8,17 @@ import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "..
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { IPhoenixConnectionInfoResult, IProvisionData, IResponse } from "../../Contracts/DataModels";
|
||||
import {
|
||||
ContainerConnectionInfo,
|
||||
IPhoenixConnectionInfoResult,
|
||||
IProvisionData,
|
||||
IResponse,
|
||||
} from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { NotebookUtil } from "./NotebookUtil";
|
||||
import { useNotebook } from "./useNotebook";
|
||||
|
||||
export class NotebookContainerClient {
|
||||
@@ -48,11 +55,21 @@ export class NotebookContainerClient {
|
||||
*/
|
||||
private scheduleHeartbeat(delayMs: number): void {
|
||||
setTimeout(async () => {
|
||||
const memoryUsageInfo = await this.getMemoryUsage();
|
||||
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
try {
|
||||
const memoryUsageInfo = await this.getMemoryUsage();
|
||||
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
}
|
||||
} catch (exception) {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Failed,
|
||||
};
|
||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
}
|
||||
}
|
||||
}, delayMs);
|
||||
}
|
||||
@@ -91,7 +108,7 @@ export class NotebookContainerClient {
|
||||
notebookServerEndpoint: string,
|
||||
authToken: string
|
||||
): Promise<DataModels.MemoryUsageInfo> {
|
||||
if (this.shouldExecuteMemoryCall()) {
|
||||
if (this.checkStatus()) {
|
||||
const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
@@ -114,17 +131,24 @@ export class NotebookContainerClient {
|
||||
} else if (response.status === HttpStatusCodes.NotFound) {
|
||||
throw new AbortError(response.statusText);
|
||||
}
|
||||
throw new Error(response.statusText);
|
||||
throw new Error();
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private shouldExecuteMemoryCall(): boolean {
|
||||
return (
|
||||
useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Active &&
|
||||
useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected
|
||||
);
|
||||
private checkStatus(): boolean {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
if (useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Disconnected) {
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Reconnect,
|
||||
};
|
||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> {
|
||||
@@ -149,8 +173,11 @@ export class NotebookContainerClient {
|
||||
}
|
||||
|
||||
try {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
const provisionData: IProvisionData = {
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
dbAccountName: userContext.databaseAccount.name,
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
};
|
||||
return await this.phoenixClient.resetContainer(provisionData);
|
||||
@@ -158,6 +185,9 @@ export class NotebookContainerClient {
|
||||
return null;
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
|
||||
if (!NotebookUtil.isPhoenixEnabled()) {
|
||||
await this.recreateNotebookWorkspaceAsync();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -172,6 +202,25 @@ export class NotebookContainerClient {
|
||||
};
|
||||
}
|
||||
|
||||
private async recreateNotebookWorkspaceAsync(): Promise<void> {
|
||||
const { databaseAccount } = userContext;
|
||||
if (!databaseAccount?.id) {
|
||||
throw new Error("DataExplorer not initialized");
|
||||
}
|
||||
try {
|
||||
await destroy(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
|
||||
await createOrUpdate(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookContainerClient/recreateNotebookWorkspaceAsync");
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
private getHeaders(): HeadersInit {
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
return {
|
||||
|
||||
@@ -3,19 +3,14 @@ import { AppState, selectors } from "@nteract/core";
|
||||
import domtoimage from "dom-to-image";
|
||||
import Html2Canvas from "html2canvas";
|
||||
import path from "path";
|
||||
import { userContext } from "../../UserContext";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import * as StringUtils from "../../Utils/StringUtils";
|
||||
import * as InMemoryContentProviderUtils from "../Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils";
|
||||
import { SnapshotFragment } from "./NotebookComponent/types";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||
|
||||
// Must match rx-jupyter' FileType
|
||||
export type FileType = "directory" | "file" | "notebook";
|
||||
export enum NotebookContentProviderType {
|
||||
GitHubContentProviderType,
|
||||
InMemoryContentProviderType,
|
||||
JupyterContentProviderType,
|
||||
}
|
||||
// Utilities for notebooks
|
||||
export class NotebookUtil {
|
||||
public static UntrustedNotebookRunHint = "Please trust notebook first before running any code cells";
|
||||
@@ -132,18 +127,6 @@ export class NotebookUtil {
|
||||
return relativePath.split("/").pop();
|
||||
}
|
||||
|
||||
public static getContentProviderType(path: string): NotebookContentProviderType {
|
||||
if (InMemoryContentProviderUtils.fromContentUri(path)) {
|
||||
return NotebookContentProviderType.InMemoryContentProviderType;
|
||||
}
|
||||
|
||||
if (GitHubUtils.fromContentUri(path)) {
|
||||
return NotebookContentProviderType.GitHubContentProviderType;
|
||||
}
|
||||
|
||||
return NotebookContentProviderType.JupyterContentProviderType;
|
||||
}
|
||||
|
||||
public static replaceName(path: string, newName: string): string {
|
||||
const contentInfo = GitHubUtils.fromContentUri(path);
|
||||
if (contentInfo) {
|
||||
@@ -346,4 +329,16 @@ export class NotebookUtil {
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
public static getNotebookBtnTitle(fileName: string): string {
|
||||
if (this.isPhoenixEnabled()) {
|
||||
return `Download to ${fileName}`;
|
||||
} else {
|
||||
return `Download to my notebooks`;
|
||||
}
|
||||
}
|
||||
|
||||
public static isPhoenixEnabled(): boolean {
|
||||
return userContext.features.notebooksTemporarilyDown === false && userContext.features.phoenix === true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -19,6 +17,7 @@ import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||
import NotebookManager from "./NotebookManager";
|
||||
import { NotebookUtil } from "./NotebookUtil";
|
||||
|
||||
interface NotebookState {
|
||||
isNotebookEnabled: boolean;
|
||||
@@ -38,8 +37,6 @@ interface NotebookState {
|
||||
isAllocating: boolean;
|
||||
isRefreshed: boolean;
|
||||
containerStatus: ContainerInfo;
|
||||
isPhoenixNotebooks: boolean;
|
||||
isPhoenixFeatures: boolean;
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
||||
@@ -61,9 +58,6 @@ interface NotebookState {
|
||||
resetContainerConnection: (connectionStatus: ContainerConnectionInfo) => void;
|
||||
setIsRefreshed: (isAllocating: boolean) => void;
|
||||
setContainerStatus: (containerStatus: ContainerInfo) => void;
|
||||
getPhoenixStatus: () => Promise<void>;
|
||||
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => void;
|
||||
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
|
||||
}
|
||||
|
||||
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
@@ -98,8 +92,6 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
durationLeftInMinutes: undefined,
|
||||
notebookServerInfo: undefined,
|
||||
},
|
||||
isPhoenixNotebooks: undefined,
|
||||
isPhoenixFeatures: undefined,
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||
@@ -112,7 +104,6 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
|
||||
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
|
||||
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
|
||||
await get().getPhoenixStatus();
|
||||
const { databaseAccount, authType } = userContext;
|
||||
if (
|
||||
authType === AuthType.EncryptedToken ||
|
||||
@@ -205,7 +196,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
|
||||
const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks";
|
||||
const notebookFolderName = NotebookUtil.isPhoenixEnabled() === true ? "Temporary Notebooks" : "My Notebooks";
|
||||
set({ notebookFolderName });
|
||||
const myNotebooksContentRoot = {
|
||||
name: get().notebookFolderName,
|
||||
@@ -301,21 +292,4 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
},
|
||||
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
|
||||
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
|
||||
getPhoenixStatus: async () => {
|
||||
if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) {
|
||||
let isPhoenix = false;
|
||||
if (userContext.features.phoenixNotebooks || userContext.features.phoenixFeatures) {
|
||||
const phoenixClient = new PhoenixClient();
|
||||
isPhoenix = isPublicInternetAccessAllowed() && (await phoenixClient.isDbAcountWhitelisted());
|
||||
}
|
||||
|
||||
const isPhoenixNotebooks = userContext.features.phoenixNotebooks && isPhoenix;
|
||||
const isPhoenixFeatures = userContext.features.phoenixFeatures && isPhoenix;
|
||||
|
||||
set({ isPhoenixNotebooks: isPhoenixNotebooks });
|
||||
set({ isPhoenixFeatures: isPhoenixFeatures });
|
||||
}
|
||||
},
|
||||
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
|
||||
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
|
||||
}));
|
||||
|
||||
@@ -25,7 +25,6 @@ export const OpenFullScreen: React.FunctionComponent = () => {
|
||||
<TextField label="Read and Write" readOnly defaultValue={readWriteUrl} />
|
||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
||||
<DefaultButton
|
||||
ariaLabel={isReadWriteUrlCopy ? "Copied url" : "Copy"}
|
||||
onClick={() => {
|
||||
copyToClipboard(readWriteUrl);
|
||||
setIsReadWriteUrlCopy(true);
|
||||
@@ -44,7 +43,6 @@ export const OpenFullScreen: React.FunctionComponent = () => {
|
||||
<TextField label="Read Only" readOnly defaultValue={readUrl} />
|
||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
||||
<DefaultButton
|
||||
ariaLabel={isReadUrlCopy ? "Copied url" : "Copy"}
|
||||
onClick={() => {
|
||||
setIsReadUrlCopy(true);
|
||||
copyToClipboard(readUrl);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Separator,
|
||||
Stack,
|
||||
Text,
|
||||
TooltipHost,
|
||||
TooltipHost
|
||||
} from "@fluentui/react";
|
||||
import * as Constants from "Common/Constants";
|
||||
import { createCollection } from "Common/dataAccess/createCollection";
|
||||
@@ -279,7 +279,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
{`${getCollectionName()} id`}
|
||||
{`${getCollectionName()} ${userContext.apiType === "Mongo" ? "name" : "id"}`}
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
@@ -468,8 +468,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
|
||||
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
|
||||
true
|
||||
).toLocaleLowerCase()} in the database and
|
||||
true
|
||||
).toLocaleLowerCase()} in the database and
|
||||
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
||||
billed in addition to the throughput amount you provisioned at the database level.`}
|
||||
>
|
||||
@@ -887,6 +887,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isServerlessAccount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "SQL":
|
||||
case "Mongo":
|
||||
@@ -1015,10 +1019,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
const partitionKeyVersion = this.state.useHashV2 ? 2 : undefined;
|
||||
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
||||
? {
|
||||
paths: [partitionKeyString],
|
||||
kind: "Hash",
|
||||
version: partitionKeyVersion,
|
||||
}
|
||||
paths: [partitionKeyString],
|
||||
kind: "Hash",
|
||||
version: partitionKeyVersion,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
|
||||
|
||||
@@ -334,7 +334,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
|
||||
isDatabase={false}
|
||||
isSharded
|
||||
isSharded={false}
|
||||
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils
|
||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
@@ -75,7 +76,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
selectedLocation.owner,
|
||||
selectedLocation.repo
|
||||
)} - ${selectedLocation.branch}`;
|
||||
} else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenixNotebooks) {
|
||||
} else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) {
|
||||
destination = useNotebook.getState().notebookFolderName;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
const onSubmit = async (): Promise<void> => {
|
||||
const collection = useSelectedNode.getState().findSelectedCollection();
|
||||
if (!collection || inputCollectionName !== collection.id()) {
|
||||
const errorMessage = "Input " + collectionName + " id does not match the selected " + collectionName;
|
||||
const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName;
|
||||
setFormError(errorMessage);
|
||||
NotificationConsoleUtils.logConsoleError(
|
||||
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IDropdownOption, IImageProps, Image, Stack, Text } from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import React, { FunctionComponent, useRef, useState } from "react";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||
@@ -25,16 +25,19 @@ interface UnwrappedExecuteSprocParam {
|
||||
export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPaneProps> = ({
|
||||
storedProcedure,
|
||||
}: ExecuteSprocParamsPaneProps): JSX.Element => {
|
||||
const paramKeyValuesRef = useRef<UnwrappedExecuteSprocParam[]>([{ key: "string", text: "" }]);
|
||||
const partitionValueRef = useRef<string>();
|
||||
const partitionKeyRef = useRef<string>("string");
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const [numberOfParams, setNumberOfParams] = useState<number>(1);
|
||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
||||
const [paramKeyValues, setParamKeyValues] = useState<UnwrappedExecuteSprocParam[]>([{ key: "string", text: "" }]);
|
||||
const [partitionValue, setPartitionValue] = useState<string>(); // Defaulting to undefined here is important. It is not the same partition key as ""
|
||||
const [selectedKey, setSelectedKey] = React.useState<IDropdownOption>({ key: "string", text: "" });
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
|
||||
const onPartitionKeyChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
|
||||
setSelectedKey(item);
|
||||
};
|
||||
|
||||
const validateUnwrappedParams = (): boolean => {
|
||||
const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValuesRef.current;
|
||||
const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValues;
|
||||
for (let i = 0; i < unwrappedParams.length; i++) {
|
||||
const { key: paramType, text: paramValue } = unwrappedParams[i];
|
||||
if (paramType === "custom" && (paramValue === "" || paramValue === undefined)) {
|
||||
@@ -50,9 +53,8 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
};
|
||||
|
||||
const submit = (): void => {
|
||||
const wrappedSprocParams: UnwrappedExecuteSprocParam[] = paramKeyValuesRef.current;
|
||||
const partitionValue: string = partitionValueRef.current;
|
||||
const partitionKey: string = partitionKeyRef.current;
|
||||
const wrappedSprocParams: UnwrappedExecuteSprocParam[] = paramKeyValues;
|
||||
const { key: partitionKey } = selectedKey;
|
||||
if (partitionKey === "custom" && (partitionValue === "" || partitionValue === undefined)) {
|
||||
setInvalidParamError(partitionValue);
|
||||
return;
|
||||
@@ -76,21 +78,37 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
};
|
||||
|
||||
const deleteParamAtIndex = (indexToRemove: number): void => {
|
||||
paramKeyValuesRef.current.splice(indexToRemove, 1);
|
||||
setNumberOfParams(numberOfParams - 1);
|
||||
const cloneParamKeyValue = [...paramKeyValues];
|
||||
cloneParamKeyValue.splice(indexToRemove, 1);
|
||||
setParamKeyValues(cloneParamKeyValue);
|
||||
};
|
||||
|
||||
const addNewParamAtIndex = (indexToAdd: number): void => {
|
||||
paramKeyValuesRef.current.splice(indexToAdd, 0, { key: "string", text: "" });
|
||||
setNumberOfParams(numberOfParams + 1);
|
||||
const cloneParamKeyValue = [...paramKeyValues];
|
||||
cloneParamKeyValue.splice(indexToAdd, 0, { key: "string", text: "" });
|
||||
setParamKeyValues(cloneParamKeyValue);
|
||||
};
|
||||
|
||||
const paramValueChange = (value: string, indexOfInput: number): void => {
|
||||
const cloneParamKeyValue = [...paramKeyValues];
|
||||
cloneParamKeyValue[indexOfInput].text = value;
|
||||
setParamKeyValues(cloneParamKeyValue);
|
||||
};
|
||||
|
||||
const paramKeyChange = (
|
||||
_event: React.FormEvent<HTMLDivElement>,
|
||||
selectedParam: IDropdownOption,
|
||||
indexOfParam: number
|
||||
): void => {
|
||||
const cloneParamKeyValue = [...paramKeyValues];
|
||||
cloneParamKeyValue[indexOfParam].key = selectedParam.key.toString();
|
||||
setParamKeyValues(cloneParamKeyValue);
|
||||
};
|
||||
|
||||
const addNewParamAtLastIndex = (): void => {
|
||||
paramKeyValuesRef.current.push({
|
||||
key: "string",
|
||||
text: "",
|
||||
});
|
||||
setNumberOfParams(numberOfParams + 1);
|
||||
const cloneParamKeyValue = [...paramKeyValues];
|
||||
cloneParamKeyValue.splice(cloneParamKeyValue.length, 0, { key: "string", text: "" });
|
||||
setParamKeyValues(cloneParamKeyValue);
|
||||
};
|
||||
|
||||
const props: RightPaneFormProps = {
|
||||
@@ -100,52 +118,46 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
onSubmit: () => submit(),
|
||||
};
|
||||
|
||||
const getInputParameterComponent = (): JSX.Element[] => {
|
||||
const inputParameters: JSX.Element[] = [];
|
||||
for (let i = 0; i < numberOfParams; i++) {
|
||||
const paramKeyValue = paramKeyValuesRef.current[i];
|
||||
inputParameters.push(
|
||||
<InputParameter
|
||||
key={paramKeyValue.text + i}
|
||||
dropdownLabel={i === 0 ? "Key" : ""}
|
||||
inputParameterTitle={i === 0 ? "Enter input parameters (if any)" : ""}
|
||||
inputLabel={i === 0 ? "Param" : ""}
|
||||
isAddRemoveVisible={true}
|
||||
onDeleteParamKeyPress={() => deleteParamAtIndex(i)}
|
||||
onAddNewParamKeyPress={() => addNewParamAtIndex(i + 1)}
|
||||
onParamValueChange={(_event, newInput?: string) => (paramKeyValuesRef.current[i].text = newInput)}
|
||||
onParamKeyChange={(_event, selectedParam: IDropdownOption) =>
|
||||
(paramKeyValuesRef.current[i].key = selectedParam.key.toString())
|
||||
}
|
||||
paramValue={paramKeyValue.text}
|
||||
selectedKey={paramKeyValue.key}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return inputParameters;
|
||||
};
|
||||
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelMainContent">
|
||||
<InputParameter
|
||||
dropdownLabel="Key"
|
||||
inputParameterTitle="Partition key value"
|
||||
inputLabel="Value"
|
||||
isAddRemoveVisible={false}
|
||||
onParamValueChange={(_event, newInput?: string) => (partitionValueRef.current = newInput)}
|
||||
onParamKeyChange={(_event: React.FormEvent<HTMLDivElement>, item: IDropdownOption) =>
|
||||
(partitionKeyRef.current = item.key.toString())
|
||||
}
|
||||
paramValue={partitionValueRef.current}
|
||||
selectedKey={partitionKeyRef.current}
|
||||
/>
|
||||
{getInputParameterComponent()}
|
||||
<Stack horizontal onClick={() => addNewParamAtLastIndex()} tabIndex={0}>
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
|
||||
<Text className="addNewParamStyle">Add New Param</Text>
|
||||
</Stack>
|
||||
<div className="panelFormWrapper">
|
||||
<div className="panelMainContent">
|
||||
<InputParameter
|
||||
dropdownLabel="Key"
|
||||
inputParameterTitle="Partition key value"
|
||||
inputLabel="Value"
|
||||
isAddRemoveVisible={false}
|
||||
onParamValueChange={(_event, newInput?: string) => {
|
||||
setPartitionValue(newInput);
|
||||
}}
|
||||
onParamKeyChange={onPartitionKeyChange}
|
||||
paramValue={partitionValue}
|
||||
selectedKey={selectedKey.key}
|
||||
/>
|
||||
{paramKeyValues.map((paramKeyValue, index) => (
|
||||
<InputParameter
|
||||
key={paramKeyValue && paramKeyValue.text + index}
|
||||
dropdownLabel={!index && "Key"}
|
||||
inputParameterTitle={!index && "Enter input parameters (if any)"}
|
||||
inputLabel={!index && "Param"}
|
||||
isAddRemoveVisible={true}
|
||||
onDeleteParamKeyPress={() => deleteParamAtIndex(index)}
|
||||
onAddNewParamKeyPress={() => addNewParamAtIndex(index + 1)}
|
||||
onParamValueChange={(event, newInput?: string) => {
|
||||
paramValueChange(newInput, index);
|
||||
}}
|
||||
onParamKeyChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
|
||||
paramKeyChange(event, selectedParam, index);
|
||||
}}
|
||||
paramValue={paramKeyValue && paramKeyValue.text}
|
||||
selectedKey={paramKeyValue && paramKeyValue.key}
|
||||
/>
|
||||
))}
|
||||
<Stack horizontal onClick={addNewParamAtLastIndex} tabIndex={0}>
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
|
||||
<Text className="addNewParamStyle">Add New Param</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
);
|
||||
|
||||
@@ -55,7 +55,7 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
|
||||
<Stack horizontal>
|
||||
<Dropdown
|
||||
label={dropdownLabel && dropdownLabel}
|
||||
defaultSelectedKey={selectedKey}
|
||||
selectedKey={selectedKey}
|
||||
onChange={onParamKeyChange}
|
||||
options={options}
|
||||
styles={dropdownStyles}
|
||||
@@ -64,9 +64,8 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
|
||||
<TextField
|
||||
label={inputLabel && inputLabel}
|
||||
id="confirmCollectionId"
|
||||
defaultValue={paramValue}
|
||||
value={paramValue}
|
||||
onChange={onParamValueChange}
|
||||
tabIndex={0}
|
||||
/>
|
||||
{isAddRemoveVisible && (
|
||||
<>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react";
|
||||
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton, Stack, Text } from "@fluentui/react";
|
||||
import * as Constants from "Common/Constants";
|
||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||
import { configContext } from "ConfigContext";
|
||||
@@ -115,15 +115,9 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
};
|
||||
|
||||
const choiceButtonStyles = {
|
||||
root: {
|
||||
clear: "both",
|
||||
},
|
||||
flexContainer: [
|
||||
{
|
||||
selectors: {
|
||||
".ms-ChoiceFieldGroup root-133": {
|
||||
clear: "both",
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": {
|
||||
fontSize: 12,
|
||||
paddingTop: 0,
|
||||
@@ -141,22 +135,22 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
{shouldShowQueryPageOptions && (
|
||||
<div className="settingsSection">
|
||||
<div className="settingsSectionPart">
|
||||
<fieldset>
|
||||
<legend id="pageOptions" className="settingsSectionLabel legendLabel">
|
||||
Page Options
|
||||
</legend>
|
||||
<Stack horizontal>
|
||||
<Text id="pageOptions" className="settingsSectionLabel" variant="small">
|
||||
Page options
|
||||
</Text>
|
||||
<InfoTooltip>
|
||||
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many
|
||||
query results per page.
|
||||
</InfoTooltip>
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="pageOptions"
|
||||
selectedKey={pageOption}
|
||||
options={pageOptionList}
|
||||
styles={choiceButtonStyles}
|
||||
onChange={handleOnPageOptionChange}
|
||||
/>
|
||||
</fieldset>
|
||||
</Stack>
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="pageOptions"
|
||||
selectedKey={pageOption}
|
||||
options={pageOptionList}
|
||||
styles={choiceButtonStyles}
|
||||
onChange={handleOnPageOptionChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="tabs settingsSectionPart">
|
||||
{isCustomPageOptionSelected() && (
|
||||
@@ -188,11 +182,14 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
{shouldShowCrossPartitionOption && (
|
||||
<div className="settingsSection">
|
||||
<div className="settingsSectionPart">
|
||||
<label className="settingsSectionLabel">Enable cross-partition query</label>
|
||||
<InfoTooltip>
|
||||
Send more than one request while executing a query. More than one request is necessary if the query is
|
||||
not scoped to single partition key value.
|
||||
</InfoTooltip>
|
||||
<div className="settingsSectionLabel">
|
||||
Enable cross-partition query
|
||||
<InfoTooltip>
|
||||
Send more than one request while executing a query. More than one request is necessary if the query is
|
||||
not scoped to single partition key value.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
@@ -208,14 +205,14 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
{shouldShowParallelismOption && (
|
||||
<div className="settingsSection">
|
||||
<div className="settingsSectionPart">
|
||||
<label className="settingsSectionLabel" htmlFor="input65">
|
||||
<div className="settingsSectionLabel">
|
||||
Max degree of parallelism
|
||||
</label>
|
||||
<InfoTooltip>
|
||||
Gets or sets the number of concurrent operations run client side during parallel query execution. A
|
||||
positive property value limits the number of concurrent operations to the set value. If it is set to
|
||||
less than 0, the system automatically decides the number of concurrent operations to run.
|
||||
</InfoTooltip>
|
||||
<InfoTooltip>
|
||||
Gets or sets the number of concurrent operations run client side during parallel query execution. A
|
||||
positive property value limits the number of concurrent operations to the set value. If it is set to
|
||||
less than 0, the system automatically decides the number of concurrent operations to run.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
|
||||
<SpinButton
|
||||
min={-1}
|
||||
|
||||
@@ -16,57 +16,54 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
<div
|
||||
className="settingsSectionPart"
|
||||
>
|
||||
<fieldset>
|
||||
<legend
|
||||
className="settingsSectionLabel legendLabel"
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<Text
|
||||
className="settingsSectionLabel"
|
||||
id="pageOptions"
|
||||
variant="small"
|
||||
>
|
||||
Page Options
|
||||
</legend>
|
||||
Page options
|
||||
</Text>
|
||||
<InfoTooltip>
|
||||
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
|
||||
</InfoTooltip>
|
||||
<StyledChoiceGroup
|
||||
ariaLabelledBy="pageOptions"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "custom",
|
||||
"text": "Custom",
|
||||
},
|
||||
Object {
|
||||
"key": "unlimited",
|
||||
"text": "Unlimited",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey="custom"
|
||||
styles={
|
||||
</Stack>
|
||||
<StyledChoiceGroup
|
||||
ariaLabelledBy="pageOptions"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField": Object {
|
||||
"marginTop": 0,
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": Object {
|
||||
"fontSize": 12,
|
||||
"paddingTop": 0,
|
||||
},
|
||||
".ms-ChoiceFieldGroup root-133": Object {
|
||||
"clear": "both",
|
||||
},
|
||||
"key": "custom",
|
||||
"text": "Custom",
|
||||
},
|
||||
Object {
|
||||
"key": "unlimited",
|
||||
"text": "Unlimited",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey="custom"
|
||||
styles={
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField": Object {
|
||||
"marginTop": 0,
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": Object {
|
||||
"fontSize": 12,
|
||||
"paddingTop": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
"root": Object {
|
||||
"clear": "both",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
/>
|
||||
</fieldset>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="tabs settingsSectionPart"
|
||||
@@ -103,14 +100,14 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
<div
|
||||
className="settingsSectionPart"
|
||||
>
|
||||
<label
|
||||
<div
|
||||
className="settingsSectionLabel"
|
||||
>
|
||||
Enable cross-partition query
|
||||
</label>
|
||||
<InfoTooltip>
|
||||
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
|
||||
</InfoTooltip>
|
||||
<InfoTooltip>
|
||||
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<StyledCheckboxBase
|
||||
ariaLabel="Enable cross partition query"
|
||||
checked={true}
|
||||
@@ -132,15 +129,14 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
<div
|
||||
className="settingsSectionPart"
|
||||
>
|
||||
<label
|
||||
<div
|
||||
className="settingsSectionLabel"
|
||||
htmlFor="input65"
|
||||
>
|
||||
Max degree of parallelism
|
||||
</label>
|
||||
<InfoTooltip>
|
||||
Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run.
|
||||
</InfoTooltip>
|
||||
<InfoTooltip>
|
||||
Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<StyledSpinButton
|
||||
ariaLabel="Max degree of parallelism"
|
||||
className="textfontclr"
|
||||
|
||||
@@ -24,7 +24,6 @@ const {
|
||||
Ascii,
|
||||
Bigint,
|
||||
Blob,
|
||||
Date: DateType,
|
||||
Decimal,
|
||||
Float,
|
||||
Int,
|
||||
@@ -42,7 +41,6 @@ export const cassandraOptions = [
|
||||
{ key: Bigint, text: Bigint },
|
||||
{ key: Blob, text: Blob },
|
||||
{ key: Boolean, text: Boolean },
|
||||
{ key: DateType, text: DateType },
|
||||
{ key: Decimal, text: Decimal },
|
||||
{ key: Double, text: Double },
|
||||
{ key: Float, text: Float },
|
||||
|
||||
@@ -84,7 +84,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
const mainItems = this.createMainItems();
|
||||
const commonTaskItems = this.createCommonTaskItems();
|
||||
let recentItems = this.createRecentItems();
|
||||
recentItems = recentItems.filter((item) => item.description !== "Notebook");
|
||||
if (userContext.features.notebooksTemporarilyDown) {
|
||||
recentItems = recentItems.filter((item) => item.description !== "Notebook");
|
||||
}
|
||||
|
||||
const tipsItems = this.createTipsItems();
|
||||
const onClearRecent = this.clearMostRecent;
|
||||
@@ -221,7 +223,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
});
|
||||
}
|
||||
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (useNotebook.getState().isNotebookEnabled && !userContext.features.notebooksTemporarilyDown) {
|
||||
heroes.push({
|
||||
iconSrc: NewNotebookIcon,
|
||||
title: "New Notebook",
|
||||
|
||||
@@ -14,7 +14,6 @@ export const CassandraType = {
|
||||
Bigint: "Bigint",
|
||||
Blob: "Blob",
|
||||
Boolean: "Boolean",
|
||||
Date: "Date",
|
||||
Decimal: "Decimal",
|
||||
Double: "Double",
|
||||
Float: "Float",
|
||||
|
||||
@@ -536,8 +536,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
dataType === TableConstants.CassandraType.Inet ||
|
||||
dataType === TableConstants.CassandraType.Ascii ||
|
||||
dataType === TableConstants.CassandraType.Varchar ||
|
||||
dataType === TableConstants.CassandraType.Timestamp ||
|
||||
dataType === TableConstants.CassandraType.Date
|
||||
dataType === TableConstants.CassandraType.Timestamp
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -364,11 +364,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
if (!this.isCloseClicked) {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
} else {
|
||||
this.isCloseClicked = false;
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (!this.isCloseClicked) {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
} else {
|
||||
this.isCloseClicked = false;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public onExecuteQueryClick = async (): Promise<void> => {
|
||||
@@ -873,11 +875,9 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="tab-pane" id={this.props.tabId} role="tabpanel">
|
||||
|
||||
@@ -25,8 +25,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Computed<boolean>;
|
||||
constructor(
|
||||
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
|
||||
private getDatabaseAccount: () => DataModels.DatabaseAccount,
|
||||
private getTabId: () => string
|
||||
private getDatabaseAccount: () => DataModels.DatabaseAccount
|
||||
) {}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
@@ -34,7 +33,6 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
||||
<NotebookTerminalComponent
|
||||
notebookServerInfo={this.getNotebookServerInfo()}
|
||||
databaseAccount={this.getDatabaseAccount()}
|
||||
tabId={this.getTabId()}
|
||||
/>
|
||||
) : (
|
||||
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
||||
@@ -52,8 +50,7 @@ export default class TerminalTab extends TabsBase {
|
||||
this.container = options.container;
|
||||
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
||||
() => this.getNotebookServerInfo(options),
|
||||
() => userContext?.databaseAccount,
|
||||
() => this.tabId
|
||||
() => userContext?.databaseAccount
|
||||
);
|
||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||
if (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { NotebookUtil } from "Explorer/Notebook/NotebookUtil";
|
||||
import * as ko from "knockout";
|
||||
import * as _ from "underscore";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -529,7 +529,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
};
|
||||
|
||||
public onSchemaAnalyzerClick = async () => {
|
||||
if (useNotebook.getState().isPhoenixFeatures) {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
await this.container.allocateContainer();
|
||||
}
|
||||
useSelectedNode.getState().setSelectedNode(this);
|
||||
|
||||
@@ -121,7 +121,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (!useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (userContext.features.notebooksTemporarilyDown) {
|
||||
notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
|
||||
} else {
|
||||
if (galleryContentRoot) {
|
||||
@@ -130,8 +130,9 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
|
||||
if (
|
||||
myNotebooksContentRoot &&
|
||||
useNotebook.getState().isPhoenixNotebooks &&
|
||||
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected
|
||||
((NotebookUtil.isPhoenixEnabled() &&
|
||||
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected) ||
|
||||
userContext.features.phoenix === false)
|
||||
) {
|
||||
notebooksTree.children.push(buildMyNotebooksTree());
|
||||
}
|
||||
@@ -165,7 +166,15 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
const myNotebooksTree: TreeNode = buildNotebookDirectoryNode(
|
||||
myNotebooksContentRoot,
|
||||
(item: NotebookContentItem) => {
|
||||
container.openNotebook(item);
|
||||
container.openNotebook(item).then((hasOpened) => {
|
||||
if (
|
||||
hasOpened &&
|
||||
userContext.features.notebooksTemporarilyDown === false &&
|
||||
userContext.features.phoenix === false
|
||||
) {
|
||||
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -180,7 +189,15 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
|
||||
gitHubNotebooksContentRoot,
|
||||
(item: NotebookContentItem) => {
|
||||
container.openNotebook(item);
|
||||
container.openNotebook(item).then((hasOpened) => {
|
||||
if (
|
||||
hasOpened &&
|
||||
userContext.features.notebooksTemporarilyDown === false &&
|
||||
userContext.features.phoenix === false
|
||||
) {
|
||||
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
|
||||
}
|
||||
});
|
||||
},
|
||||
true
|
||||
);
|
||||
@@ -380,11 +397,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
},
|
||||
];
|
||||
|
||||
//disallow renaming of temporary notebook workspace
|
||||
if (item?.path === useNotebook.getState().notebookBasePath) {
|
||||
items = items.filter((item) => item.label !== "Rename");
|
||||
}
|
||||
|
||||
// For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File"
|
||||
if (GitHubUtils.fromContentUri(item.path)) {
|
||||
items = items.filter(
|
||||
@@ -516,7 +528,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
isNotebookEnabled &&
|
||||
userContext.apiType === "Mongo" &&
|
||||
isPublicInternetAccessAllowed() &&
|
||||
useNotebook.getState().isPhoenixFeatures
|
||||
!userContext.features.notebooksTemporarilyDown
|
||||
) {
|
||||
children.push({
|
||||
label: "Schema (Preview)",
|
||||
|
||||
@@ -808,11 +808,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
},
|
||||
];
|
||||
|
||||
//disallow renaming of temporary notebook workspace
|
||||
if (item?.path === useNotebook.getState().notebookBasePath) {
|
||||
items = items.filter((item) => item.label !== "Rename");
|
||||
}
|
||||
|
||||
// For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File"
|
||||
if (GitHubUtils.fromContentUri(item.path)) {
|
||||
items = items.filter(
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import {
|
||||
Areas,
|
||||
ConnectionStatusType,
|
||||
ContainerStatusType,
|
||||
HttpHeaders,
|
||||
HttpStatusCodes,
|
||||
Notebook,
|
||||
} from "../Common/Constants";
|
||||
import { ContainerStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../Common/Constants";
|
||||
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import {
|
||||
ContainerConnectionInfo,
|
||||
ContainerInfo,
|
||||
IContainerData,
|
||||
IPhoenixConnectionInfoResult,
|
||||
@@ -20,7 +11,6 @@ import {
|
||||
IResponse,
|
||||
} from "../Contracts/DataModels";
|
||||
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
|
||||
@@ -45,8 +35,8 @@ export class PhoenixClient {
|
||||
operation: string
|
||||
): Promise<IResponse<IPhoenixConnectionInfoResult>> {
|
||||
try {
|
||||
const response = await fetch(`${this.getPhoenixControlPlanePathPrefix()}/containerconnections`, {
|
||||
method: operation === "allocate" ? "POST" : "PATCH",
|
||||
const response = await fetch(`${this.getPhoenixContainerPoolingEndPoint()}/${operation}`, {
|
||||
method: "POST",
|
||||
headers: PhoenixClient.getHeaders(),
|
||||
body: JSON.stringify(provisionData),
|
||||
});
|
||||
@@ -65,7 +55,7 @@ export class PhoenixClient {
|
||||
}
|
||||
}
|
||||
|
||||
public async initiateContainerHeartBeat(containerData: IContainerData) {
|
||||
public async initiateContainerHeartBeat(containerData: { forwardingId: string; dbAccountName: string }) {
|
||||
if (this.containerHealthHandler) {
|
||||
clearTimeout(this.containerHealthHandler);
|
||||
}
|
||||
@@ -82,7 +72,7 @@ export class PhoenixClient {
|
||||
try {
|
||||
const runContainerStatusAsync = async () => {
|
||||
const response = await window.fetch(
|
||||
`${this.getPhoenixControlPlanePathPrefix()}/${containerData.forwardingId}`,
|
||||
`${this.getPhoenixContainerPoolingEndPoint()}/${containerData.dbAccountName}/${containerData.forwardingId}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: PhoenixClient.getHeaders(),
|
||||
@@ -96,32 +86,13 @@ export class PhoenixClient {
|
||||
status: ContainerStatusType.Active,
|
||||
};
|
||||
} else if (response.status === HttpStatusCodes.NotFound) {
|
||||
const error = "Disconnected from compute workspace";
|
||||
Logger.logError(error, "");
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Reconnect,
|
||||
};
|
||||
TelemetryProcessor.traceMark(Action.PhoenixHeartBeat, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
message: getErrorMessage(error),
|
||||
});
|
||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
throw new AbortError(response.statusText);
|
||||
}
|
||||
throw new Error(response.statusText);
|
||||
};
|
||||
return await promiseRetry(runContainerStatusAsync, this.retryOptions);
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(Action.PhoenixHeartBeat, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
Logger.logError(getErrorMessage(error), "");
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Failed,
|
||||
};
|
||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
Logger.logError(getErrorMessage(error), "PhoenixClient/getContainerStatus");
|
||||
return {
|
||||
durationLeftInMinutes: undefined,
|
||||
notebookServerInfo: undefined,
|
||||
@@ -130,24 +101,19 @@ export class PhoenixClient {
|
||||
}
|
||||
}
|
||||
|
||||
private async getContainerHealth(delayMs: number, containerData: IContainerData) {
|
||||
const containerInfo = await this.getContainerStatusAsync(containerData);
|
||||
useNotebook.getState().setContainerStatus(containerInfo);
|
||||
if (useNotebook.getState().containerStatus?.status === ContainerStatusType.Active) {
|
||||
this.scheduleContainerHeartbeat(delayMs, containerData);
|
||||
}
|
||||
}
|
||||
|
||||
public async isDbAcountWhitelisted(): Promise<boolean> {
|
||||
private async getContainerHealth(delayMs: number, containerData: { forwardingId: string; dbAccountName: string }) {
|
||||
try {
|
||||
const response = await window.fetch(`${this.getPhoenixControlPlanePathPrefix()}`, {
|
||||
method: "GET",
|
||||
headers: PhoenixClient.getHeaders(),
|
||||
const containerInfo = await this.getContainerStatusAsync(containerData);
|
||||
useNotebook.getState().setContainerStatus(containerInfo);
|
||||
if (useNotebook.getState().containerStatus?.status === ContainerStatusType.Active) {
|
||||
this.scheduleContainerHeartbeat(delayMs, containerData);
|
||||
}
|
||||
} catch (exception) {
|
||||
useNotebook.getState().setContainerStatus({
|
||||
durationLeftInMinutes: undefined,
|
||||
notebookServerInfo: undefined,
|
||||
status: ContainerStatusType.Disconnected,
|
||||
});
|
||||
return response.status === HttpStatusCodes.OK;
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "PhoenixClient/IsDbAcountWhitelisted");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,10 +129,8 @@ export class PhoenixClient {
|
||||
return phoenixEndpoint;
|
||||
}
|
||||
|
||||
public getPhoenixControlPlanePathPrefix(): string {
|
||||
return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer/cosmosaccounts${
|
||||
userContext.databaseAccount.id
|
||||
}`;
|
||||
public getPhoenixContainerPoolingEndPoint(): string {
|
||||
return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer`;
|
||||
}
|
||||
|
||||
private static getHeaders(): HeadersInit {
|
||||
|
||||
@@ -11,8 +11,7 @@ export type Features = {
|
||||
autoscaleDefault: boolean;
|
||||
partitionKeyDefault: boolean;
|
||||
partitionKeyDefault2: boolean;
|
||||
phoenixNotebooks: boolean;
|
||||
phoenixFeatures: boolean;
|
||||
phoenix: boolean;
|
||||
notebooksDownBanner: boolean;
|
||||
readonly enableSDKoperations: boolean;
|
||||
readonly enableSpark: boolean;
|
||||
@@ -34,6 +33,7 @@ export type Features = {
|
||||
readonly ttl90Days: boolean;
|
||||
readonly mongoProxyEndpoint?: string;
|
||||
readonly mongoProxyAPIs?: string;
|
||||
readonly notebooksTemporarilyDown: boolean;
|
||||
readonly enableThroughputCap: boolean;
|
||||
};
|
||||
|
||||
@@ -84,8 +84,8 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
autoscaleDefault: "true" === get("autoscaledefault"),
|
||||
partitionKeyDefault: "true" === get("partitionkeytest"),
|
||||
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
|
||||
phoenixNotebooks: "true" === get("phoenixnotebooks"),
|
||||
phoenixFeatures: "true" === get("phoenixfeatures"),
|
||||
notebooksTemporarilyDown: "true" === get("notebookstemporarilydown", "true"),
|
||||
phoenix: "true" === get("phoenix"),
|
||||
notebooksDownBanner: "true" === get("notebooksDownBanner"),
|
||||
enableThroughputCap: "true" === get("enablethroughputcap"),
|
||||
};
|
||||
|
||||
@@ -82,7 +82,6 @@ export enum Action {
|
||||
NotebooksMoveCellUpFromMenu,
|
||||
NotebooksMoveCellDownFromMenu,
|
||||
PhoenixConnection,
|
||||
PhoenixHeartBeat,
|
||||
PhoenixResetWorkspace,
|
||||
DeleteCellFromMenu,
|
||||
OpenTerminal,
|
||||
|
||||
@@ -2,61 +2,15 @@
|
||||
* 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 {
|
||||
private isShellStarted: boolean | undefined;
|
||||
private checkShellStarted: ((content: string | undefined) => void) | undefined;
|
||||
private onShellExited: () => void;
|
||||
|
||||
private isShellExited(content: string | undefined) {
|
||||
return content?.includes("cosmosuser@");
|
||||
}
|
||||
|
||||
private isMongoShellStarted(content: string | undefined) {
|
||||
this.isShellStarted = content?.includes("MongoDB shell version");
|
||||
}
|
||||
|
||||
private isCassandraShellStarted(content: string | undefined) {
|
||||
this.isShellStarted = content?.includes("Connected to") && content?.includes("cqlsh");
|
||||
}
|
||||
|
||||
constructor(closeTab: () => void) {
|
||||
this.onShellExited = closeTab;
|
||||
this.isShellStarted = false;
|
||||
this.checkShellStarted = undefined;
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "Mongo":
|
||||
this.checkShellStarted = this.isMongoShellStarted;
|
||||
break;
|
||||
case "Cassandra":
|
||||
this.checkShellStarted = this.isCassandraShellStarted;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async createTerminalApp(serverSettings: ServerConnection.ISettings) {
|
||||
public static 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.checkShellStarted && message.type == "stdout") {
|
||||
//Close the terminal tab once the shell closed messages are received
|
||||
if (!this.isShellStarted) {
|
||||
this.checkShellStarted(content);
|
||||
} else if (this.isShellExited(content)) {
|
||||
this.onShellExited();
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
const term = new Terminal(session, { theme: "dark", shutdownOnClose: true });
|
||||
|
||||
if (!term) {
|
||||
|
||||
@@ -10,5 +10,4 @@ export interface TerminalProps {
|
||||
authType: AuthType;
|
||||
apiType: ApiType;
|
||||
subscriptionId: string;
|
||||
tabId: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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";
|
||||
@@ -55,20 +54,13 @@ const initTerminal = async (props: TerminalProps) => {
|
||||
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
|
||||
|
||||
try {
|
||||
await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings);
|
||||
await JupyterLabAppFactory.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<void> => {
|
||||
postRobot.on(
|
||||
"props",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
SortBy,
|
||||
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
||||
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
@@ -228,7 +229,7 @@ export function downloadItem(
|
||||
undefined,
|
||||
"Download",
|
||||
async () => {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
await container.allocateContainer();
|
||||
}
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { applyExplorerBindings } from "../applyExplorerBindings";
|
||||
import { AuthType } from "../AuthType";
|
||||
@@ -70,38 +69,16 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
||||
|
||||
async function configureHosted(): Promise<Explorer> {
|
||||
const win = (window as unknown) as HostedExplorerChildFrame;
|
||||
let explorer: Explorer;
|
||||
if (win.hostedConfig.authType === AuthType.EncryptedToken) {
|
||||
explorer = configureHostedWithEncryptedToken(win.hostedConfig);
|
||||
return configureHostedWithEncryptedToken(win.hostedConfig);
|
||||
} else if (win.hostedConfig.authType === AuthType.ResourceToken) {
|
||||
explorer = configureHostedWithResourceToken(win.hostedConfig);
|
||||
return configureHostedWithResourceToken(win.hostedConfig);
|
||||
} else if (win.hostedConfig.authType === AuthType.ConnectionString) {
|
||||
explorer = configureHostedWithConnectionString(win.hostedConfig);
|
||||
return configureHostedWithConnectionString(win.hostedConfig);
|
||||
} else if (win.hostedConfig.authType === AuthType.AAD) {
|
||||
explorer = await configureHostedWithAAD(win.hostedConfig);
|
||||
} else {
|
||||
throw new Error(`Unknown hosted config: ${win.hostedConfig}`);
|
||||
return configureHostedWithAAD(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;
|
||||
throw new Error(`Unknown hosted config: ${win.hostedConfig}`);
|
||||
}
|
||||
|
||||
async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
|
||||
@@ -284,8 +261,6 @@ async function configurePortal(): Promise<Explorer> {
|
||||
}
|
||||
} 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
|
||||
@@ -364,11 +339,8 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) {
|
||||
userContext.features.partitionKeyDefault2 = true;
|
||||
}
|
||||
if (inputs.flights.indexOf(Flights.PhoenixNotebooks) !== -1) {
|
||||
userContext.features.phoenixNotebooks = true;
|
||||
}
|
||||
if (inputs.flights.indexOf(Flights.PhoenixFeatures) !== -1) {
|
||||
userContext.features.phoenixFeatures = true;
|
||||
if (inputs.flights.indexOf(Flights.Phoenix) !== -1) {
|
||||
userContext.features.phoenix = true;
|
||||
}
|
||||
if (inputs.flights.indexOf(Flights.NotebooksDownBanner) !== -1) {
|
||||
userContext.features.notebooksDownBanner = true;
|
||||
|
||||
Reference in New Issue
Block a user