Compare commits

..

5 Commits

Author SHA1 Message Date
kcheekuri
0fe1e2940e Resolve compile errors 2021-11-10 14:22:30 -05:00
kcheekuri
f9d91061a7 Change endpoint 2021-11-10 12:56:08 -05:00
kcheekuri
4a50d138cd Merge branch 'master' into users/kcheekuri/removePhoenixFlag 2021-11-10 11:39:41 -05:00
kcheekuri
a7409553d4 Remove phoenix and notebook temporary down features and apply phoenix for whitelisted acounts 2021-11-10 10:52:41 -05:00
Srinath Narayanan
a9d38cdb3b fixed quickstarts 2021-11-09 13:01:24 -05:00
57 changed files with 5525 additions and 35484 deletions

View File

@@ -1,4 +1,3 @@
{
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
"isTerminalEnabled" : true
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com"
}

View File

@@ -1,4 +1,3 @@
{
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"isTerminalEnabled" : false
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com"
}

View File

@@ -2077,7 +2077,7 @@ a:link {
.resourceTreeAndTabs {
display: flex;
flex: 1 1 auto;
overflow-x: clip;
overflow-x: auto;
overflow-y: auto;
height: 100%;
}
@@ -2245,7 +2245,7 @@ a:link {
}
.refreshColHeader {
padding: 3px 6px 10px 0px !important;
padding: 3px 6px 6px 6px;
}
.refreshColHeader:hover {
@@ -2869,43 +2869,31 @@ a:link {
}
}
.settingsSection {
border-bottom: 1px solid @BaseMedium;
margin-right: 24px;
padding: @MediumSpace 0px;
settings-pane {
.settingsSection {
border-bottom: 1px solid @BaseMedium;
margin-right: 24px;
padding: @MediumSpace 0px;
&:first-child {
padding-top: 0px;
padding-bottom: 10px;
}
&:last-child {
border-bottom: none;
}
.settingsSectionPart {
padding-left: 8px;
label {
padding: 0 !important;
font-size: inherit;
&:first-child {
padding-top: 0px;
}
}
.settingsSectionLabel {
margin-bottom: @DefaultSpace;
margin-right: 5px;
}
&:last-child {
border-bottom: none;
}
.pageOptionsPart {
padding-bottom: @MediumSpace;
}
.settingsSectionPart {
padding-left: 8px;
}
.legendLabel {
border-bottom: 0px;
width: auto;
font-size: @mediumFontSize;
display: inline !important;
float: left;
.settingsSectionLabel {
margin-bottom: @DefaultSpace;
}
.pageOptionsPart {
padding-bottom: @MediumSpace;
}
}
}

29666
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -96,8 +96,6 @@ 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 NotebooksDownBanner = "notebooksdownbanner";
}
@@ -366,7 +364,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 +377,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 =
@@ -413,11 +411,3 @@ export class TerminalQueryParams {
public static readonly SubscriptionId = "subscriptionId";
public static readonly TerminalEndpoint = "terminalEndpoint";
}
export class JunoEndpoints {
public static readonly Test = "https://juno-test.documents-dev.windows-int.net";
public static readonly Test2 = "https://juno-test2.documents-dev.windows-int.net";
public static readonly Test3 = "https://juno-test3.documents-dev.windows-int.net";
public static readonly Prod = "https://tools.cosmos.azure.com";
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
}

View File

@@ -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) {

View File

@@ -1,5 +1,3 @@
import { JunoEndpoints } from "Common/Constants";
export enum Platform {
Portal = "Portal",
Hosted = "Hosted",
@@ -25,9 +23,7 @@ export interface ConfigContext {
PROXY_PATH?: string;
JUNO_ENDPOINT: string;
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[];
@@ -56,17 +52,15 @@ let configContext: Readonly<ConfigContext> = {
GRAPH_API_VERSION: "1.6",
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
isTerminalEnabled: false,
allowedJunoOrigins: [
JunoEndpoints.Test,
JunoEndpoints.Test2,
JunoEndpoints.Test3,
JunoEndpoints.Prod,
JunoEndpoints.Stage,
"https://juno-test.documents-dev.windows-int.net",
"https://juno-test2.documents-dev.windows-int.net",
"https://juno-test3.documents-dev.windows-int.net",
"https://tools.cosmos.azure.com",
"https://tools-staging.cosmos.azure.com",
"https://localhost",
],
};

View File

@@ -441,7 +441,13 @@ export interface IProvisionData {
cosmosEndpoint: string;
}
export interface IContainerData {
export interface IAccountData {
subscriptionId: string;
resourceGroup: string;
dbAccountName: string;
}
export interface IContainerData extends IAccountData {
forwardingId: string;
}

View File

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

View File

@@ -83,6 +83,7 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: HostedTerminalIcon,
isDisabled: useNotebook.getState().isShellEnabled && useNotebook.getState().isPhoenix === false,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {

View File

@@ -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} />);

View File

@@ -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, {

View File

@@ -213,8 +213,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
},
};
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
if (userContext.databaseAccount?.properties.capacity?.totalThroughputLimit) {
this.calculateTotalThroughputUsed();
}
}
@@ -681,7 +680,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
const throughputDelta = (newThroughput - this.offer.autoscaleMaxThroughput) * numberOfRegions;
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
if (throughputCap && throughputCap - this.totalThroughputUsed < throughputDelta) {
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
this.totalThroughputUsed + throughputDelta
} RU/s. Change total throughput limit in cost management.`;
@@ -694,7 +693,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
const throughputDelta = (newThroughput - this.offer.manualThroughput) * numberOfRegions;
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
if (throughputCap && throughputCap - this.totalThroughputUsed < newThroughput - this.offer.manualThroughput) {
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
this.totalThroughputUsed + throughputDelta
} RU/s. Change total throughput limit in cost management.`;

View File

@@ -61,7 +61,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
totalThroughput *= numberOfRegions;
setTotalThroughputUsed(totalThroughput);
if (throughputCap && throughputCap !== -1 && throughputCap - totalThroughput < throughput) {
if (throughputCap && throughputCap - totalThroughput < throughput) {
setThroughputError(
`Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
totalThroughput + throughput * numberOfRegions
@@ -73,7 +73,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
}, []);
const checkThroughputCap = (newThroughput: number): boolean => {
if (throughputCap && throughputCap !== -1 && throughputCap - totalThroughputUsed < newThroughput) {
if (throughputCap && throughputCap - totalThroughputUsed < newThroughput) {
setThroughputError(
`Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
totalThroughputUsed + newThroughput * numberOfRegions

View File

@@ -7,7 +7,7 @@ import shallow from "zustand/shallow";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants";
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
@@ -16,6 +16,8 @@ import { QueriesClient } from "../Common/QueriesClient";
import * as DataModels from "../Contracts/DataModels";
import {
ContainerConnectionInfo,
IAccountData,
IContainerData,
IPhoenixConnectionInfoResult,
IProvisionData,
IResponse,
@@ -351,7 +353,6 @@ export default class Explorer {
if (!databaseAccount) {
throw new Error("No database account specified");
}
if (this._isInitializingNotebooks) {
return;
}
@@ -371,41 +372,28 @@ export default class Explorer {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
};
const accountData: IAccountData = {
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
dbAccountName: userContext.databaseAccount.name,
};
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
try {
TelemetryProcessor.traceStart(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
});
useNotebook.getState().setIsAllocating(true);
const connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
if (connectionInfo.status !== HttpStatusCodes.OK) {
throw new Error(`Received status code: ${connectionInfo?.status}`);
}
if (!connectionInfo?.data?.notebookServerUrl) {
throw new Error(`NotebookServerUrl is invalid!`);
}
const connectionInfo = await this.phoenixClient.allocateContainer(provisionData, accountData);
await this.setNotebookInfo(connectionInfo, connectionStatus);
TelemetryProcessor.traceSuccess(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
});
} catch (error) {
TelemetryProcessor.traceFailure(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetContainerConnection(connectionStatus);
throw error;
} finally {
useNotebook.getState().setIsAllocating(false);
this.refreshCommandBarButtons();
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
this.refreshCommandBarButtons();
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
}
@@ -413,22 +401,30 @@ export default class Explorer {
connectionInfo: IResponse<IPhoenixConnectionInfoResult>,
connectionStatus: DataModels.ContainerConnectionInfo
) {
const containerData = {
forwardingId: connectionInfo.data.forwardingId,
dbAccountName: userContext.databaseAccount.name,
};
await this.phoenixClient.initiateContainerHeartBeat(containerData);
if (connectionInfo.status === HttpStatusCodes.OK && connectionInfo.data && connectionInfo.data.notebookServerUrl) {
const containerData: IContainerData = {
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
dbAccountName: userContext.databaseAccount.name,
forwardingId: connectionInfo.data.forwardingId,
};
await this.phoenixClient.initiateContainerHeartBeat(containerData);
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
forwardingId: connectionInfo.data.forwardingId,
});
this.notebookManager?.notebookClient
.getMemoryUsage()
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
forwardingId: connectionInfo.data.forwardingId,
});
this.notebookManager?.notebookClient
.getMemoryUsage()
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
} else {
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetContainerConnection(connectionStatus);
}
useNotebook.getState().setIsAllocating(false);
}
public resetNotebookWorkspace(): void {
@@ -439,14 +435,10 @@ export default class Explorer {
);
return;
}
const dialogContent = useNotebook.getState().isPhoenixNotebooks
? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?"
: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?";
const resetConfirmationDialogProps: DialogProps = {
isModal: true,
title: "Reset Workspace",
subText: dialogContent,
subText: "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?",
primaryButtonText: "OK",
secondaryButtonText: "Cancel",
onPrimaryButtonClick: this._resetNotebookWorkspace,
@@ -513,45 +505,39 @@ export default class Explorer {
logConsoleError(error);
return;
}
TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
});
if (useNotebook.getState().isPhoenixNotebooks) {
useTabs.getState().closeAllNotebookTabs(true);
connectionStatus = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
}
useTabs.getState().closeAllNotebookTabs(true);
connectionStatus = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace();
if (connectionInfo?.status !== HttpStatusCodes.OK) {
throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`);
}
if (!connectionInfo?.data?.notebookServerUrl) {
throw new Error(`Reset Workspace: NotebookServerUrl is invalid!`);
}
if (useNotebook.getState().isPhoenixNotebooks) {
await this.setNotebookInfo(connectionInfo, connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
logConsoleInfo("Successfully reset notebook workspace");
TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
});
} catch (error) {
logConsoleError(`Failed to reset notebook workspace: ${error}`);
TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
if (useNotebook.getState().isPhoenixNotebooks) {
if (connectionInfo && connectionInfo.status && connectionInfo.status === HttpStatusCodes.OK) {
if (connectionInfo.data && connectionInfo.data.notebookServerUrl) {
await this.setNotebookInfo(connectionInfo, connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
logConsoleInfo("Successfully reset notebook workspace");
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
} else {
logConsoleError(`Failed to reset notebook workspace`);
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace);
connectionStatus = {
status: ConnectionStatusType.Failed,
status: ConnectionStatusType.Reconnect,
};
useNotebook.getState().resetContainerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
} catch (error) {
logConsoleError(`Failed to reset notebook workspace: ${error}`);
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, {
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
connectionStatus = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
throw error;
} finally {
clearInProgressMessage();
@@ -743,7 +729,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 && useNotebook.getState().isPhoenix) {
await this.allocateContainer();
}
@@ -961,17 +947,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 = useNotebook.getState().isPhoenix;
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 +1045,7 @@ export default class Explorer {
}
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
if (useNotebook.getState().isPhoenixFeatures) {
if (useNotebook.getState().isPhoenix) {
await this.allocateContainer();
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
@@ -1096,7 +1085,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) {
@@ -1168,8 +1157,7 @@ export default class Explorer {
<CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} />
);
} else {
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
throughputCap && throughputCap !== -1
userContext.databaseAccount?.properties.capacity?.totalThroughputLimit
? await useDatabases.getState().loadAllOffers()
: await useDatabases.getState().loadDatabaseOffers();
useSidePanel
@@ -1197,7 +1185,7 @@ export default class Explorer {
}
public async handleOpenFileAction(path: string): Promise<void> {
if (useNotebook.getState().isPhoenixNotebooks) {
if (useNotebook.getState().isPhoenix) {
await this.allocateContainer();
} else if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
this._openSetupNotebooksPaneForQuickstart();
@@ -1231,7 +1219,7 @@ export default class Explorer {
}
public openUploadFilePanel(parent?: NotebookContentItem): void {
if (useNotebook.getState().isPhoenixNotebooks) {
if (useNotebook.getState().isPhoenix) {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookUploadModalTitle,
undefined,
@@ -1245,9 +1233,6 @@ export default class Explorer {
undefined,
this.getNewNoteWarningText()
);
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
}
}
@@ -1261,7 +1246,7 @@ export default class Explorer {
}
public getDownloadModalConent(fileName: string): JSX.Element {
if (useNotebook.getState().isPhoenixNotebooks) {
if (useNotebook.getState().isPhoenix) {
return (
<>
<p>{Notebook.galleryNotebookDownloadContent1}</p>
@@ -1285,19 +1270,21 @@ 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 || useNotebook.getState().isPhoenix;
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook
.getState()
.setIsShellEnabled(useNotebook.getState().isPhoenixFeatures && isPublicInternetAccessAllowed());
useNotebook.getState().setIsShellEnabled(useNotebook.getState().isPhoenix && isPublicInternetAccessAllowed());
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
isNotebookEnabled,
dataExplorerArea: Constants.Areas.Notebook,
});
if (useNotebook.getState().isPhoenixNotebooks) {
await this.initNotebooks(userContext.databaseAccount);
if (useNotebook.getState().isPhoenix) {
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();
}
}
}
}

View File

@@ -53,7 +53,7 @@ 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 (useNotebook.getState().isPhoenix) {
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
}

View File

@@ -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", () => {
@@ -179,29 +194,26 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsPhoenix(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined();
//TODO: modify once notebooks are available
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
expect(openMongoShellBtn.disabled).toBe(false);
expect(openMongoShellBtn.tooltipText).toBe("");
});
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsPhoenix(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined();
//TODO: modify once notebooks are available
expect(openMongoShellBtn.disabled).toBe(true);
//expect(openMongoShellBtn.disabled).toBe(false);
//expect(openMongoShellBtn.tooltipText).toBe("");
expect(openMongoShellBtn.disabled).toBe(false);
expect(openMongoShellBtn.tooltipText).toBe("");
});
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
@@ -284,30 +296,27 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsPhoenix(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined();
//TODO: modify once notebooks are available
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
expect(openCassandraShellBtn.disabled).toBe(false);
expect(openCassandraShellBtn.tooltipText).toBe("");
});
it("Notebooks is enabled and is available - button should be shown and enabled", () => {
useNotebook.getState().setIsNotebookEnabled(true);
useNotebook.getState().setIsPhoenix(true);
useNotebook.getState().setIsNotebooksEnabledForAccount(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined();
//TODO: modify once notebooks are available
expect(openCassandraShellBtn.disabled).toBe(true);
//expect(openCassandraShellBtn.disabled).toBe(false);
//expect(openCassandraShellBtn.tooltipText).toBe("");
expect(openCassandraShellBtn.disabled).toBe(false);
expect(openCassandraShellBtn.tooltipText).toBe("");
});
});

View File

@@ -10,7 +10,6 @@ import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import GitHubIcon from "../../../../images/github.svg";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg";
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
@@ -25,6 +24,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 +77,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,24 +97,17 @@ export function createStaticCommandBarButtons(
}
notebookButtons.forEach((btn) => {
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
if (useNotebook.getState().isPhoenix === false && userContext.features.notebooksDownBanner) {
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) {
buttons.push(createDivider());
buttons.push(createEnableNotebooksButton(container));
}
}
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
@@ -170,7 +162,9 @@ export function createContextCommandBarButtons(
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
if (useNotebook.getState().isPhoenix) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
}
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
@@ -178,6 +172,13 @@ export function createContextCommandBarButtons(
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
tooltipText:
useNotebook.getState().isShellEnabled && useNotebook.getState().isPhoenix === false
? Constants.Notebook.mongoShellTemporarilyDownMsg
: undefined,
disabled:
(selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") ||
(useNotebook.getState().isShellEnabled && useNotebook.getState().isPhoenix === false),
};
buttons.push(newMongoShellBtn);
}
@@ -273,6 +274,10 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
return undefined;
}
if (isServerlessAccount()) {
return undefined;
}
if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) {
return undefined;
}
@@ -300,8 +305,7 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
iconSrc: AddDatabaseIcon,
iconAlt: label,
onCommandClick: async () => {
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
if (userContext.databaseAccount?.properties.capacity?.totalThroughputLimit) {
await useDatabases.getState().loadAllOffers();
}
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />);
@@ -466,33 +470,6 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
};
}
function createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return undefined;
}
const label = "Enable Notebooks (Preview)";
const tooltip =
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const description =
"Looks like you have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
return {
iconSrc: EnableNotebooksIcon,
iconAlt: label,
onCommandClick: () =>
useSidePanel
.getState()
.openSidePanel(
label,
<SetupNoteBooksPanel explorer={container} panelTitle={label} panelDescription={description} />
),
commandButtonLabel: label,
hasPopup: false,
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
ariaLabel: label,
tooltipText: useNotebook.getState().isNotebooksEnabledForAccount ? "" : tooltip,
};
}
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Terminal";
return {

View File

@@ -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>
</>
);
}
}

View File

@@ -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,
];

View File

@@ -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,

View File

@@ -8,8 +8,15 @@ 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,
IAccountData,
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 { useNotebook } from "./useNotebook";
@@ -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 (useNotebook.getState().isPhoenix) {
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: {
@@ -120,11 +137,18 @@ export class NotebookContainerClient {
}
}
private shouldExecuteMemoryCall(): boolean {
return (
useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Active &&
useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected
);
private checkStatus(): boolean {
if (useNotebook.getState().isPhoenix) {
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,11 +173,16 @@ export class NotebookContainerClient {
}
try {
if (useNotebook.getState().isPhoenixNotebooks) {
if (useNotebook.getState().isPhoenix) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
};
return await this.phoenixClient.resetContainer(provisionData);
const accountData: IAccountData = {
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
dbAccountName: userContext.databaseAccount.name,
};
return await this.phoenixClient.resetContainer(provisionData, accountData);
}
return null;
} catch (error) {
@@ -172,6 +201,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 {

View File

@@ -5,17 +5,11 @@ import Html2Canvas from "html2canvas";
import path from "path";
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 +126,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) {

View File

@@ -1,4 +1,3 @@
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { cloneDeep } from "lodash";
import { PhoenixClient } from "Phoenix/PhoenixClient";
import create, { UseStore } from "zustand";
@@ -9,7 +8,7 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
import { ContainerConnectionInfo, ContainerInfo, IAccountData } from "../../Contracts/DataModels";
import { useTabs } from "../../hooks/useTabs";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
@@ -38,8 +37,7 @@ interface NotebookState {
isAllocating: boolean;
isRefreshed: boolean;
containerStatus: ContainerInfo;
isPhoenixNotebooks: boolean;
isPhoenixFeatures: boolean;
isPhoenix: boolean;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
@@ -62,8 +60,7 @@ interface NotebookState {
setIsRefreshed: (isAllocating: boolean) => void;
setContainerStatus: (containerStatus: ContainerInfo) => void;
getPhoenixStatus: () => Promise<void>;
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => void;
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
setIsPhoenix: (isPhoenix: boolean) => void;
}
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
@@ -98,8 +95,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
durationLeftInMinutes: undefined,
notebookServerInfo: undefined,
},
isPhoenixNotebooks: undefined,
isPhoenixFeatures: undefined,
isPhoenix: undefined,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
@@ -205,7 +201,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 = "Temporary Notebooks";
set({ notebookFolderName });
const myNotebooksContentRoot = {
name: get().notebookFolderName,
@@ -302,20 +298,16 @@ 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 });
if (get().isPhoenix === undefined) {
const phoenixClient = new PhoenixClient();
const accountData: IAccountData = {
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
dbAccountName: userContext.databaseAccount.name,
};
const isPhoenix = await phoenixClient.IsDbAcountWhitelisted(accountData);
set({ isPhoenix });
}
},
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
setIsPhoenix: (isPhoenix: boolean) => set({ isPhoenix }),
}));

View File

@@ -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);

View File

@@ -279,7 +279,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{`${getCollectionName()} id`}
{`${getCollectionName()} ${userContext.apiType === "Mongo" ? "name" : "id"}`}
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
@@ -667,7 +667,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{userContext.apiType === "SQL" && (
<Checkbox
label="My partition key is larger than 101 bytes"
label="My partition key is larger than 100 bytes"
checked={this.state.useHashV2}
styles={{
text: { fontSize: 12 },
@@ -887,6 +887,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false;
}
if (isServerlessAccount()) {
return false;
}
switch (userContext.apiType) {
case "SQL":
case "Mongo":

View File

@@ -23,12 +23,10 @@ import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneFor
export interface AddDatabasePaneProps {
explorer: Explorer;
buttonElement?: HTMLElement;
}
export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
explorer: container,
buttonElement,
}: AddDatabasePaneProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
let throughput: number;
@@ -80,9 +78,6 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
dataExplorerArea: Constants.Areas.ContextualPane,
};
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
if (buttonElement) {
buttonElement.focus();
}
}, []);
const onSubmit = () => {

View File

@@ -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)}

View File

@@ -75,7 +75,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") {
destination = useNotebook.getState().notebookFolderName;
}

View File

@@ -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}`

View File

@@ -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>
);

View File

@@ -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 && (
<>

View File

@@ -113,50 +113,20 @@ export const SettingsPane: FunctionComponent = () => {
const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
setPageOption(option.key);
};
const choiceButtonStyles = {
root: {
clear: "both",
},
flexContainer: [
{
selectors: {
".ms-ChoiceFieldGroup root-133": {
clear: "both",
},
".ms-ChoiceField-wrapper label": {
fontSize: 12,
paddingTop: 0,
},
".ms-ChoiceField": {
marginTop: 0,
},
},
},
],
};
return (
<RightPaneForm {...genericPaneProps}>
<div className="paneMainContent">
{shouldShowQueryPageOptions && (
<div className="settingsSection">
<div className="settingsSectionPart">
<fieldset>
<legend id="pageOptions" className="settingsSectionLabel legendLabel">
Page Options
</legend>
<div className="settingsSectionPart pageOptionsPart">
<div className="settingsSectionLabel">
Page options
<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>
</div>
<ChoiceGroup selectedKey={pageOption} options={pageOptionList} onChange={handleOnPageOptionChange} />
</div>
<div className="tabs settingsSectionPart">
{isCustomPageOptionSelected() && (
@@ -188,11 +158,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 +181,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}

View File

@@ -14,59 +14,32 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSection"
>
<div
className="settingsSectionPart"
className="settingsSectionPart pageOptionsPart"
>
<fieldset>
<legend
className="settingsSectionLabel legendLabel"
id="pageOptions"
>
Page Options
</legend>
<div
className="settingsSectionLabel"
>
Page options
<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={
</div>
<StyledChoiceGroup
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",
},
},
},
],
"root": Object {
"clear": "both",
},
}
}
/>
</fieldset>
"key": "custom",
"text": "Custom",
},
Object {
"key": "unlimited",
"text": "Unlimited",
},
]
}
selectedKey="custom"
/>
</div>
<div
className="tabs settingsSectionPart"
@@ -103,14 +76,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 +105,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"

View File

@@ -24,7 +24,6 @@ const {
Ascii,
Bigint,
Blob,
Date: DateType,
Decimal,
Float,
Int,
@@ -34,7 +33,6 @@ const {
Inet,
Smallint,
Tinyint,
Timestamp,
} = TableConstants.CassandraType;
export const cassandraOptions = [
{ key: Text, text: Text },
@@ -42,7 +40,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 },
@@ -53,7 +50,6 @@ export const cassandraOptions = [
{ key: Inet, text: Inet },
{ key: Smallint, text: Smallint },
{ key: Tinyint, text: Tinyint },
{ key: Timestamp, text: Timestamp },
];
export const imageProps: IImageProps = {

View File

@@ -85,7 +85,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
const commonTaskItems = this.createCommonTaskItems();
let recentItems = this.createRecentItems();
recentItems = recentItems.filter((item) => item.description !== "Notebook");
const tipsItems = this.createTipsItems();
const onClearRecent = this.clearMostRecent;
@@ -221,7 +220,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
});
}
if (useNotebook.getState().isPhoenixNotebooks) {
if (useNotebook.getState().isPhoenix) {
heroes.push({
iconSrc: NewNotebookIcon,
title: "New Notebook",
@@ -306,16 +305,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: "New " + getDatabaseName(),
description: undefined,
onClick: async () => {
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
if (userContext.databaseAccount?.properties.capacity?.totalThroughputLimit) {
await useDatabases.getState().loadAllOffers();
}
useSidePanel
.getState()
.openSidePanel(
"New " + getDatabaseName(),
<AddDatabasePanel explorer={this.container} buttonElement={document.activeElement as HTMLElement} />
);
.openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={this.container} />);
},
});
}

View File

@@ -14,13 +14,11 @@ export const CassandraType = {
Bigint: "Bigint",
Blob: "Blob",
Boolean: "Boolean",
Date: "Date",
Decimal: "Decimal",
Double: "Double",
Float: "Float",
Int: "Int",
Text: "Text",
Timestamp: "Timestamp",
Uuid: "Uuid",
Varchar: "Varchar",
Varint: "Varint",

View File

@@ -431,7 +431,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
if (newHeaders.length > 0) {
// Any new columns found will be added into headers array, which will trigger a re-render of the DataTable.
// So there is no need to call it here.
this.updateHeaders(selectedHeadersUnion, /* notifyColumnChanges */ true);
this.updateHeaders(newHeaders, /* notifyColumnChanges */ true);
} else {
if (columnSortOrder) {
this.sortColumns(columnSortOrder, oSettings);

View File

@@ -535,9 +535,7 @@ export class CassandraAPIDataClient extends TableDataClient {
dataType === TableConstants.CassandraType.Text ||
dataType === TableConstants.CassandraType.Inet ||
dataType === TableConstants.CassandraType.Ascii ||
dataType === TableConstants.CassandraType.Varchar ||
dataType === TableConstants.CassandraType.Timestamp ||
dataType === TableConstants.CassandraType.Date
dataType === TableConstants.CassandraType.Varchar
);
}

View File

@@ -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">

View File

@@ -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 (

View File

@@ -529,7 +529,7 @@ export default class Collection implements ViewModels.Collection {
};
public onSchemaAnalyzerClick = async () => {
if (useNotebook.getState().isPhoenixFeatures) {
if (useNotebook.getState().isPhoenix) {
await this.container.allocateContainer();
}
useSelectedNode.getState().setSelectedNode(this);
@@ -576,8 +576,9 @@ export default class Collection implements ViewModels.Collection {
public onSettingsClick = async (): Promise<void> => {
useSelectedNode.getState().setSelectedNode(this);
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await this.loadOffer();
userContext.databaseAccount?.properties.capacity?.totalThroughputLimit
? await useDatabases.getState().loadAllOffers()
: await this.loadOffer();
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Settings node",

View File

@@ -66,8 +66,7 @@ export default class Database implements ViewModels.Database {
dataExplorerArea: Constants.Areas.ResourceTree,
});
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
if (userContext.databaseAccount?.properties.capacity?.totalThroughputLimit) {
await useDatabases.getState().loadAllOffers();
}

View File

@@ -121,16 +121,16 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
children: [],
};
if (!useNotebook.getState().isPhoenixNotebooks) {
if (useNotebook.getState().isPhoenix === false && userContext.features.notebooksDownBanner) {
notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
} else {
} else if (useNotebook.getState().isPhoenix) {
if (galleryContentRoot) {
notebooksTree.children.push(buildGalleryNotebooksTree());
}
if (
myNotebooksContentRoot &&
useNotebook.getState().isPhoenixNotebooks &&
useNotebook.getState().isPhoenix &&
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected
) {
notebooksTree.children.push(buildMyNotebooksTree());
@@ -380,11 +380,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 +511,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
isNotebookEnabled &&
userContext.apiType === "Mongo" &&
isPublicInternetAccessAllowed() &&
useNotebook.getState().isPhoenixFeatures
useNotebook.getState().isPhoenix
) {
children.push({
label: "Schema (Preview)",

View File

@@ -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(

View File

@@ -1,8 +1,8 @@
import ko from "knockout";
import postRobot from "post-robot";
import { GetGithubClientId } from "Utils/GitHubUtils";
import { HttpStatusCodes } from "../Common/Constants";
import { handleError } from "../Common/ErrorHandlingUtils";
import { configContext } from "../ConfigContext";
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
import { JunoClient } from "../Juno/JunoClient";
import { logConsoleInfo } from "../Utils/NotificationConsoleUtils";
@@ -55,7 +55,7 @@ export class GitHubOAuthService {
const params = {
scope,
client_id: GetGithubClientId(),
client_id: configContext.GITHUB_CLIENT_ID,
redirect_uri: new URL("./connectToGitHub.html", window.location.href).href,
state: this.resetState(),
};

View File

@@ -1,5 +1,4 @@
import ko from "knockout";
import { GetGithubClientId } from "Utils/GitHubUtils";
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
@@ -523,7 +522,7 @@ export class JunoClient {
private static getGitHubClientParams(): URLSearchParams {
const githubParams = new URLSearchParams({
client_id: GetGithubClientId(),
client_id: configContext.GITHUB_CLIENT_ID,
});
if (configContext.GITHUB_CLIENT_SECRET) {

View File

@@ -1,26 +1,17 @@
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,
IAccountData,
IContainerData,
IPhoenixConnectionInfoResult,
IProvisionData,
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";
@@ -32,24 +23,36 @@ export class PhoenixClient {
minTimeout: Notebook.retryAttemptDelayMs,
};
public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixConnectionInfoResult>> {
return this.executeContainerAssignmentOperation(provisionData, "allocate");
public async allocateContainer(
provisionData: IProvisionData,
accountData: IAccountData
): Promise<IResponse<IPhoenixConnectionInfoResult>> {
return this.executeContainerAssignmentOperation(provisionData, accountData, "allocate");
}
public async resetContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixConnectionInfoResult>> {
return this.executeContainerAssignmentOperation(provisionData, "reset");
public async resetContainer(
provisionData: IProvisionData,
accountData: IAccountData
): Promise<IResponse<IPhoenixConnectionInfoResult>> {
return this.executeContainerAssignmentOperation(provisionData, accountData, "reset");
}
private async executeContainerAssignmentOperation(
provisionData: IProvisionData,
accountData: IAccountData,
operation: string
): Promise<IResponse<IPhoenixConnectionInfoResult>> {
try {
const response = await fetch(`${this.getPhoenixControlPlanePathPrefix()}/containerconnections`, {
method: operation === "allocate" ? "POST" : "PATCH",
headers: PhoenixClient.getHeaders(),
body: JSON.stringify(provisionData),
});
const response = await fetch(
`${this.getPhoenixContainerPoolingEndPoint()}/subscriptions/${accountData.subscriptionId}/resourceGroups/${
accountData.resourceGroup
}/providers/Microsoft.DocumentDB/databaseAccounts/${accountData.dbAccountName}/containerconnections`,
{
method: operation === "allocate" ? "POST" : "PATCH",
headers: PhoenixClient.getHeaders(),
body: JSON.stringify(provisionData),
}
);
let data: IPhoenixConnectionInfoResult;
if (response.status === HttpStatusCodes.OK) {
@@ -82,7 +85,11 @@ export class PhoenixClient {
try {
const runContainerStatusAsync = async () => {
const response = await window.fetch(
`${this.getPhoenixControlPlanePathPrefix()}/${containerData.forwardingId}`,
`${this.getPhoenixContainerPoolingEndPoint()}/subscriptions/${containerData.subscriptionId}/resourceGroups/${
containerData.resourceGroup
}/providers/Microsoft.DocumentDB/databaseAccounts/${containerData.dbAccountName}/${
containerData.forwardingId
}`,
{
method: "GET",
headers: PhoenixClient.getHeaders(),
@@ -96,32 +103,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,
@@ -131,19 +119,32 @@ 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);
try {
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,
});
}
}
public async isDbAcountWhitelisted(): Promise<boolean> {
public async IsDbAcountWhitelisted(accountData: IAccountData) {
try {
const response = await window.fetch(`${this.getPhoenixControlPlanePathPrefix()}`, {
method: "GET",
headers: PhoenixClient.getHeaders(),
});
const response = await window.fetch(
`${this.getPhoenixContainerPoolingEndPoint()}/subscriptions/${accountData.subscriptionId}/resourceGroups/${
accountData.resourceGroup
}/providers/Microsoft.DocumentDB/databaseAccounts/${accountData.dbAccountName}`,
{
method: "GET",
headers: PhoenixClient.getHeaders(),
}
);
return response.status === HttpStatusCodes.OK;
} catch (error) {
Logger.logError(getErrorMessage(error), "PhoenixClient/IsDbAcountWhitelisted");
@@ -163,10 +164,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/cosmosaccounts`;
}
private static getHeaders(): HeadersInit {

View File

@@ -11,8 +11,6 @@ export type Features = {
autoscaleDefault: boolean;
partitionKeyDefault: boolean;
partitionKeyDefault2: boolean;
phoenixNotebooks: boolean;
phoenixFeatures: boolean;
notebooksDownBanner: boolean;
readonly enableSDKoperations: boolean;
readonly enableSpark: boolean;
@@ -84,8 +82,6 @@ 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"),
notebooksDownBanner: "true" === get("notebooksDownBanner"),
enableThroughputCap: "true" === get("enablethroughputcap"),
};

View File

@@ -50,6 +50,7 @@ export enum Action {
SubscriptionSwitch,
TenantSwitch,
DefaultTenantSwitch,
ResetNotebookWorkspace,
CreateNotebookWorkspace,
NotebookErrorNotification,
CreateSparkCluster,
@@ -81,9 +82,6 @@ export enum Action {
NotebooksInsertTextCellBelowFromMenu,
NotebooksMoveCellUpFromMenu,
NotebooksMoveCellDownFromMenu,
PhoenixConnection,
PhoenixHeartBeat,
PhoenixResetWorkspace,
DeleteCellFromMenu,
OpenTerminal,
CreateMongoCollectionWithWildcardIndex,

View File

@@ -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) {

View File

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

View File

@@ -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",

View File

@@ -228,7 +228,7 @@ export function downloadItem(
undefined,
"Download",
async () => {
if (useNotebook.getState().isPhoenixNotebooks) {
if (useNotebook.getState().isPhoenix) {
await container.allocateContainer();
}
const notebookServerInfo = useNotebook.getState().notebookServerInfo;

View File

@@ -1,9 +1,4 @@
// https://github.com/<owner>/<repo>/tree/<branch>
import { JunoEndpoints } from "Common/Constants";
import { configContext } from "ConfigContext";
import { userContext } from "UserContext";
// The url when users visit a repo/branch on github.com
export const RepoUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/tree\/([^?]*)/;
@@ -65,15 +60,3 @@ export function toContentUri(owner: string, repo: string, branch: string, path:
export function toRawContentUri(owner: string, repo: string, branch: string, path: string): string {
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
}
export function GetGithubClientId(): string {
const junoEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
if (
junoEndpoint === JunoEndpoints.Test ||
junoEndpoint === JunoEndpoints.Test2 ||
junoEndpoint === JunoEndpoints.Test3
) {
return configContext.GITHUB_TEST_ENV_CLIENT_ID;
}
return configContext.GITHUB_CLIENT_ID;
}

View File

@@ -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,12 +339,6 @@ 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.NotebooksDownBanner) !== -1) {
userContext.features.notebooksDownBanner = true;
}