Merge branch 'master' into users/srnara/selfServeDemo

This commit is contained in:
Srinath Narayanan
2021-05-19 09:42:09 +05:30
76 changed files with 14688 additions and 20261 deletions
-4
View File
@@ -111,13 +111,9 @@ src/Explorer/OpenActionsStubs.ts
src/Explorer/Panes/AddDatabasePane.ts src/Explorer/Panes/AddDatabasePane.ts
src/Explorer/Panes/AddDatabasePane.test.ts src/Explorer/Panes/AddDatabasePane.test.ts
src/Explorer/Panes/BrowseQueriesPane.ts src/Explorer/Panes/BrowseQueriesPane.ts
src/Explorer/Panes/CassandraAddCollectionPane.ts
src/Explorer/Panes/ContextualPaneBase.ts src/Explorer/Panes/ContextualPaneBase.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
# src/Explorer/Panes/GraphStylingPane.ts # src/Explorer/Panes/GraphStylingPane.ts
# src/Explorer/Panes/NewVertexPane.ts # src/Explorer/Panes/NewVertexPane.ts
src/Explorer/Panes/PaneComponents.ts
src/Explorer/Panes/RenewAdHocAccessPane.ts src/Explorer/Panes/RenewAdHocAccessPane.ts
src/Explorer/Panes/SetupNotebooksPane.ts src/Explorer/Panes/SetupNotebooksPane.ts
src/Explorer/Panes/SwitchDirectoryPane.ts src/Explorer/Panes/SwitchDirectoryPane.ts
+8 -1
View File
@@ -10,6 +10,13 @@ const _global = typeof self === "undefined" ? window : self;
export const tokenProvider = async (requestInfo: RequestInfo) => { export const tokenProvider = async (requestInfo: RequestInfo) => {
const { verb, resourceId, resourceType, headers } = requestInfo; const { verb, resourceId, resourceType, headers } = requestInfo;
if (userContext.features.enableAadDataPlane && userContext.aadToken) {
const AUTH_PREFIX = `type=aad&ver=1.0&sig=`;
const authorizationToken = `${AUTH_PREFIX}${userContext.aadToken}`;
return authorizationToken;
}
if (configContext.platform === Platform.Emulator) { if (configContext.platform === Platform.Emulator) {
// TODO This SDK method mutates the headers object. Find a better one or fix the SDK. // TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey); await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
@@ -76,7 +83,7 @@ export function client(): Cosmos.CosmosClient {
if (_client) return _client; if (_client) return _client;
const options: Cosmos.CosmosClientOptions = { 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 endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
key: userContext.masterKey, ...(!userContext.features.enableAadDataPlane && { key: userContext.masterKey }),
tokenProvider, tokenProvider,
connectionPolicy: { connectionPolicy: {
enableEndpointDiscovery: false, enableEndpointDiscovery: false,
+1 -1
View File
@@ -1,8 +1,8 @@
import { CollectionBase } from "../../Contracts/ViewModels"; import { CollectionBase } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility"; import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
export const createDocument = async (collection: CollectionBase, newDocument: unknown): Promise<unknown> => { export const createDocument = async (collection: CollectionBase, newDocument: unknown): Promise<unknown> => {
const entityName = getEntityName(); const entityName = getEntityName();
+1 -1
View File
@@ -1,6 +1,6 @@
import { Queries } from "../Constants";
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Queries } from "../Constants";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
export const queryDocuments = ( export const queryDocuments = (
+2 -2
View File
@@ -1,8 +1,8 @@
import { QueryResults } from "../../Contracts/ViewModels"; import { QueryResults } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { MinimalQueryIterator, nextPage } from "../IteratorUtilities";
import { handleError } from "../ErrorHandlingUtils";
import { getEntityName } from "../DocumentUtility"; import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { MinimalQueryIterator, nextPage } from "../IteratorUtilities";
export const queryDocumentsPage = async ( export const queryDocumentsPage = async (
resourceName: string, resourceName: string,
+6
View File
@@ -20,6 +20,8 @@ export interface DatabaseAccountExtendedProperties {
writeLocations?: DatabaseAccountResponseLocation[]; writeLocations?: DatabaseAccountResponseLocation[];
enableFreeTier?: boolean; enableFreeTier?: boolean;
enableAnalyticalStorage?: boolean; enableAnalyticalStorage?: boolean;
isVirtualNetworkFilterEnabled?: boolean;
ipRules?: IpRule[];
} }
export interface DatabaseAccountResponseLocation { export interface DatabaseAccountResponseLocation {
@@ -31,6 +33,10 @@ export interface DatabaseAccountResponseLocation {
provisioningState: string; provisioningState: string;
} }
export interface IpRule {
ipAddressOrRange: string;
}
export interface ConfigurationOverrides { export interface ConfigurationOverrides {
EnableBsonSchema: string; EnableBsonSchema: string;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
import * as Versions from "./Versions";
import * as ActionContracts from "./ActionContracts"; import * as ActionContracts from "./ActionContracts";
import * as Diagnostics from "./Diagnostics"; import * as Diagnostics from "./Diagnostics";
import * as Versions from "./Versions";
/** /**
* Messaging types used with Data Explorer <-> Portal communication * Messaging types used with Data Explorer <-> Portal communication
-4
View File
@@ -4,13 +4,9 @@ import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponen
import { EditorComponent } from "./Controls/Editor/EditorComponent"; import { EditorComponent } from "./Controls/Editor/EditorComponent";
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import * as PaneComponents from "./Panes/PaneComponents";
ko.components.register("editor", new EditorComponent()); ko.components.register("editor", new EditorComponent());
ko.components.register("json-editor", new JsonEditorComponent()); ko.components.register("json-editor", new JsonEditorComponent());
ko.components.register("diff-editor", new DiffEditorComponent()); ko.components.register("diff-editor", new DiffEditorComponent());
ko.components.register("dynamic-list", DynamicListComponent); ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3); ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
// Panes
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
+6 -2
View File
@@ -73,9 +73,13 @@ export class ResourceTreeContextMenuButtonFactory {
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
onClick: () => { onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoShellClick(); if (container.isShellEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
}, },
label: "New Shell", label: container.isShellEnabled() ? "Open Mongo Shell" : "New Shell",
}); });
} }
@@ -1,12 +1,12 @@
jest.mock("../../../../Juno/JunoClient"); jest.mock("../../../../Juno/JunoClient");
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react"; import React from "react";
import { CodeOfConductComponent, CodeOfConductComponentProps } from ".";
import { HttpStatusCodes } from "../../../../Common/Constants"; import { HttpStatusCodes } from "../../../../Common/Constants";
import { JunoClient } from "../../../../Juno/JunoClient"; import { JunoClient } from "../../../../Juno/JunoClient";
import { CodeOfConduct, CodeOfConductProps } from "./CodeOfConduct";
describe("CodeOfConductComponent", () => { describe("CodeOfConduct", () => {
let codeOfConductProps: CodeOfConductComponentProps; let codeOfConductProps: CodeOfConductProps;
beforeEach(() => { beforeEach(() => {
const junoClient = new JunoClient(); const junoClient = new JunoClient();
@@ -21,12 +21,12 @@ describe("CodeOfConductComponent", () => {
}); });
it("renders", () => { it("renders", () => {
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />); const wrapper = shallow(<CodeOfConduct {...codeOfConductProps} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("onAcceptedCodeOfConductCalled", async () => { it("onAcceptedCodeOfConductCalled", async () => {
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />); const wrapper = shallow(<CodeOfConduct {...codeOfConductProps} />);
wrapper.find(".genericPaneSubmitBtn").first().simulate("click"); wrapper.find(".genericPaneSubmitBtn").first().simulate("click");
await Promise.resolve(); await Promise.resolve();
expect(codeOfConductProps.onAcceptCodeOfConduct).toBeCalled(); expect(codeOfConductProps.onAcceptCodeOfConduct).toBeCalled();
@@ -6,15 +6,15 @@ import { JunoClient } from "../../../../Juno/JunoClient";
import { Action } from "../../../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../../../Shared/Telemetry/TelemetryConstants";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../../Shared/Telemetry/TelemetryProcessor"; import { trace, traceFailure, traceStart, traceSuccess } from "../../../../Shared/Telemetry/TelemetryProcessor";
export interface CodeOfConductComponentProps { export interface CodeOfConductProps {
junoClient: JunoClient; junoClient: JunoClient;
onAcceptCodeOfConduct: (result: boolean) => void; onAcceptCodeOfConduct: (result: boolean) => void;
} }
export const CodeOfConductComponent: FunctionComponent<CodeOfConductComponentProps> = ({ export const CodeOfConduct: FunctionComponent<CodeOfConductProps> = ({
junoClient, junoClient,
onAcceptCodeOfConduct, onAcceptCodeOfConduct,
}: CodeOfConductComponentProps) => { }: CodeOfConductProps) => {
const descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct"; const descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct";
const descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB."; const descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.";
const descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the "; const descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the ";
@@ -47,7 +47,7 @@ export const CodeOfConductComponent: FunctionComponent<CodeOfConductComponentPro
startKey startKey
); );
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct"); handleError(error, "CodeOfConduct/acceptCodeOfConduct", "Failed to accept code of conduct");
} }
}; };
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CodeOfConductComponent renders 1`] = ` exports[`CodeOfConduct renders 1`] = `
<Stack <Stack
tokens={ tokens={
Object { Object {
@@ -1,123 +0,0 @@
import * as React from "react";
import { JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
import { Stack, Text, Checkbox, PrimaryButton, Link } from "@fluentui/react";
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
export interface CodeOfConductComponentProps {
junoClient: JunoClient;
onAcceptCodeOfConduct: (result: boolean) => void;
}
interface CodeOfConductComponentState {
readCodeOfConduct: boolean;
}
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
private viewCodeOfConductTraced: boolean;
private descriptionPara1: string;
private descriptionPara2: string;
private descriptionPara3: string;
private link1: { label: string; url: string };
constructor(props: CodeOfConductComponentProps) {
super(props);
this.state = {
readCodeOfConduct: false,
};
this.descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct";
this.descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.";
this.descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the ";
this.link1 = { label: "code of conduct.", url: CodeOfConductEndpoints.codeOfConduct };
}
private async acceptCodeOfConduct(): Promise<void> {
const startKey = traceStart(Action.NotebooksGalleryAcceptCodeOfConduct);
try {
const response = await this.props.junoClient.acceptCodeOfConduct();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
}
traceSuccess(Action.NotebooksGalleryAcceptCodeOfConduct, {}, startKey);
this.props.onAcceptCodeOfConduct(response.data);
} catch (error) {
traceFailure(
Action.NotebooksGalleryAcceptCodeOfConduct,
{
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct");
}
}
private onChangeCheckbox = (): void => {
this.setState({ readCodeOfConduct: !this.state.readCodeOfConduct });
};
public render(): JSX.Element {
if (!this.viewCodeOfConductTraced) {
this.viewCodeOfConductTraced = true;
trace(Action.NotebooksGalleryViewCodeOfConduct);
}
return (
<Stack tokens={{ childrenGap: 20 }}>
<Stack.Item>
<Text style={{ fontWeight: 500, fontSize: "20px" }}>{this.descriptionPara1}</Text>
</Stack.Item>
<Stack.Item>
<Text>{this.descriptionPara2}</Text>
</Stack.Item>
<Stack.Item>
<Text>
{this.descriptionPara3}
<Link href={this.link1.url} target="_blank">
{this.link1.label}
</Link>
</Text>
</Stack.Item>
<Stack.Item>
<Checkbox
styles={{
label: {
margin: 0,
padding: "2 0 2 0",
},
text: {
fontSize: 12,
},
}}
label="I have read and accept the code of conduct."
onChange={this.onChangeCheckbox}
/>
</Stack.Item>
<Stack.Item>
<PrimaryButton
ariaLabel="Continue"
title="Continue"
onClick={async () => await this.acceptCodeOfConduct()}
tabIndex={0}
className="genericPaneSubmitBtn"
text="Continue"
disabled={!this.state.readCodeOfConduct}
/>
</Stack.Item>
</Stack>
);
}
}
@@ -30,7 +30,7 @@ import * as GalleryUtils from "../../../Utils/GalleryUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { Dialog, DialogProps } from "../Dialog"; import { Dialog, DialogProps } from "../Dialog";
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent"; import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
import { CodeOfConductComponent } from "./CodeOfConductComponent"; import { CodeOfConduct } from "./CodeOfConduct/CodeOfConduct";
import "./GalleryViewerComponent.less"; import "./GalleryViewerComponent.less";
import { InfoComponent } from "./InfoComponent/InfoComponent"; import { InfoComponent } from "./InfoComponent/InfoComponent";
@@ -372,7 +372,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
{acceptedCodeOfConduct === false && ( {acceptedCodeOfConduct === false && (
<Overlay isDarkThemed> <Overlay isDarkThemed>
<div className="publicGalleryTabOverlayContent"> <div className="publicGalleryTabOverlayContent">
<CodeOfConductComponent <CodeOfConduct
junoClient={this.props.junoClient} junoClient={this.props.junoClient}
onAcceptCodeOfConduct={(result: boolean) => { onAcceptCodeOfConduct={(result: boolean) => {
this.setState({ isCodeOfConductAccepted: result }); this.setState({ isCodeOfConductAccepted: result });
@@ -38,51 +38,6 @@ exports[`SettingsComponent renders 1`] = `
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
"closeDialog": undefined, "closeDialog": undefined,
"closeSidePanel": undefined, "closeSidePanel": undefined,
"collapsedResourceTreeWidth": 36, "collapsedResourceTreeWidth": 36,
@@ -887,6 +842,7 @@ exports[`SettingsComponent renders 1`] = `
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function], "isSchemaEnabled": [Function],
"isServerlessEnabled": [Function], "isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSparkEnabled": [Function], "isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
@@ -978,51 +934,6 @@ exports[`SettingsComponent renders 1`] = `
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
"closeDialog": undefined, "closeDialog": undefined,
"closeSidePanel": undefined, "closeSidePanel": undefined,
"collapsedResourceTreeWidth": 36, "collapsedResourceTreeWidth": 36,
@@ -1827,6 +1738,7 @@ exports[`SettingsComponent renders 1`] = `
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function], "isSchemaEnabled": [Function],
"isServerlessEnabled": [Function], "isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSparkEnabled": [Function], "isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
@@ -1931,51 +1843,6 @@ exports[`SettingsComponent renders 1`] = `
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
"closeDialog": undefined, "closeDialog": undefined,
"closeSidePanel": undefined, "closeSidePanel": undefined,
"collapsedResourceTreeWidth": 36, "collapsedResourceTreeWidth": 36,
@@ -2780,6 +2647,7 @@ exports[`SettingsComponent renders 1`] = `
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function], "isSchemaEnabled": [Function],
"isServerlessEnabled": [Function], "isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSparkEnabled": [Function], "isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
@@ -2871,51 +2739,6 @@ exports[`SettingsComponent renders 1`] = `
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
"closeDialog": undefined, "closeDialog": undefined,
"closeSidePanel": undefined, "closeSidePanel": undefined,
"collapsedResourceTreeWidth": 36, "collapsedResourceTreeWidth": 36,
@@ -3720,6 +3543,7 @@ exports[`SettingsComponent renders 1`] = `
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function], "isSchemaEnabled": [Function],
"isServerlessEnabled": [Function], "isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSparkEnabled": [Function], "isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
+47 -38
View File
@@ -54,7 +54,7 @@ import { NotebookUtil } from "./Notebook/NotebookUtil";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel"; import { AddDatabasePanel } from "./Panes/AddDatabasePanel/AddDatabasePanel";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane"; import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane/BrowseQueriesPane";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
@@ -67,7 +67,7 @@ import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksP
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel"; import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel";
import { EditTableEntityPanel } from "./Panes/Tables/EditTableEntityPanel"; import { EditTableEntityPanel } from "./Panes/Tables/EditTableEntityPanel";
import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel"; import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel";
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel"; import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel";
@@ -148,7 +148,6 @@ export default class Explorer {
public tabsManager: TabsManager; public tabsManager: TabsManager;
// Contextual panes // Contextual panes
public cassandraAddCollectionPane: CassandraAddCollectionPane;
private gitHubClient: GitHubClient; private gitHubClient: GitHubClient;
public gitHubOAuthService: GitHubOAuthService; public gitHubOAuthService: GitHubOAuthService;
public junoClient: JunoClient; public junoClient: JunoClient;
@@ -178,6 +177,8 @@ export default class Explorer {
public openDialog: ExplorerParams["openDialog"]; public openDialog: ExplorerParams["openDialog"];
public closeDialog: ExplorerParams["closeDialog"]; public closeDialog: ExplorerParams["closeDialog"];
public isShellEnabled: ko.Observable<boolean>;
private _isInitializingNotebooks: boolean; private _isInitializingNotebooks: boolean;
private notebookBasePath: ko.Observable<string>; private notebookBasePath: ko.Observable<string>;
private _arcadiaManager: ArcadiaResourceManager; private _arcadiaManager: ArcadiaResourceManager;
@@ -223,6 +224,7 @@ export default class Explorer {
}); });
} }
}); });
this.isShellEnabled = ko.observable(false);
this.isNotebooksEnabledForAccount = ko.observable(false); this.isNotebooksEnabledForAccount = ko.observable(false);
this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons()); this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons());
this.isSparkEnabledForAccount = ko.observable(false); this.isSparkEnabledForAccount = ko.observable(false);
@@ -249,6 +251,12 @@ export default class Explorer {
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) || ((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
userContext.features.enableNotebooks) userContext.features.enableNotebooks)
); );
this.isShellEnabled(
this.isNotebookEnabled() &&
!userContext.databaseAccount.properties.isVirtualNetworkFilterEnabled &&
userContext.databaseAccount.properties.ipRules.length === 0
);
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
isNotebookEnabled: this.isNotebookEnabled(), isNotebookEnabled: this.isNotebookEnabled(),
dataExplorerArea: Constants.Areas.Notebook, dataExplorerArea: Constants.Areas.Notebook,
@@ -398,13 +406,6 @@ export default class Explorer {
} }
}); });
this.cassandraAddCollectionPane = new CassandraAddCollectionPane({
id: "cassandraaddcollectionpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.tabsManager = params?.tabsManager ?? new TabsManager(); this.tabsManager = params?.tabsManager ?? new TabsManager();
this.tabsManager.openedTabs.subscribe((tabs) => { this.tabsManager.openedTabs.subscribe((tabs) => {
if (tabs.length === 0) { if (tabs.length === 0) {
@@ -1138,7 +1139,10 @@ export default class Explorer {
private getDeltaDatabases( private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[] updatedDatabaseList: DataModels.Database[]
): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } { ): {
toAdd: ViewModels.Database[];
toDelete: ViewModels.Database[];
} {
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
const databaseExists = _.some( const databaseExists = _.some(
this.databases(), this.databases(),
@@ -1384,7 +1388,7 @@ export default class Explorer {
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
} else { } else {
this.openSidePanel( this.openSidePanel(
"", "Rename Notebook",
<StringInputPane <StringInputPane
explorer={this} explorer={this}
closePanel={() => { closePanel={() => {
@@ -1415,7 +1419,7 @@ export default class Explorer {
} }
this.openSidePanel( this.openSidePanel(
"", "Create new directory",
<StringInputPane <StringInputPane
explorer={this} explorer={this}
closePanel={() => { closePanel={() => {
@@ -1720,32 +1724,27 @@ export default class Explorer {
throw new Error("Terminal kind: ${kind} not supported"); throw new Error("Terminal kind: ${kind} not supported");
} }
const terminalTabs: TerminalTab[] = this.tabsManager.getTabs( const terminalTabs: TerminalTab[] = this.tabsManager.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) =>
ViewModels.CollectionTabKind.Terminal, tab.hashLocation().startsWith(hashLocation)
(tab) => tab.hashLocation() == hashLocation
) as TerminalTab[]; ) as TerminalTab[];
let terminalTab: TerminalTab = terminalTabs && terminalTabs[0];
if (terminalTab) { const index = terminalTabs.length + 1;
this.tabsManager.activateTab(terminalTab); const newTab = new TerminalTab({
} else { account: userContext.databaseAccount,
const newTab = new TerminalTab({ tabKind: ViewModels.CollectionTabKind.Terminal,
account: userContext.databaseAccount, node: null,
tabKind: ViewModels.CollectionTabKind.Terminal, title: `${title} ${index}`,
node: null, tabPath: `${title} ${index}`,
title: title, collection: null,
tabPath: title, hashLocation: `${hashLocation} ${index}`,
collection: null, isTabsContentExpanded: ko.observable(true),
hashLocation: hashLocation, onLoadStartKey: null,
isTabsContentExpanded: ko.observable(true), onUpdateTabsButtons: this.onUpdateTabsButtons,
onLoadStartKey: null, container: this,
onUpdateTabsButtons: this.onUpdateTabsButtons, kind: kind,
container: this, });
kind: kind,
});
this.tabsManager.activateNewTab(newTab); this.tabsManager.activateNewTab(newTab);
}
} }
public async openGallery( public async openGallery(
@@ -1791,7 +1790,7 @@ export default class Explorer {
public onNewCollectionClicked(databaseId?: string): void { public onNewCollectionClicked(databaseId?: string): void {
if (userContext.apiType === "Cassandra") { if (userContext.apiType === "Cassandra") {
this.cassandraAddCollectionPane.open(); this.openCassandraAddCollectionPane();
} else { } else {
this.openAddCollectionPanel(databaseId); this.openAddCollectionPanel(databaseId);
} }
@@ -1907,7 +1906,7 @@ export default class Explorer {
"Delete " + getDatabaseName(), "Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel <DeleteDatabaseConfirmationPanel
explorer={this} explorer={this}
openNotificationConsole={this.expandConsole} openNotificationConsole={() => this.expandConsole()}
closePanel={this.closeSidePanel} closePanel={this.closeSidePanel}
selectedDatabase={this.findSelectedDatabase()} selectedDatabase={this.findSelectedDatabase()}
/> />
@@ -1983,6 +1982,16 @@ export default class Explorer {
); );
} }
public openCassandraAddCollectionPane(): void {
this.openSidePanel(
"Add Table",
<CassandraAddCollectionPane
explorer={this}
closePanel={() => this.closeSidePanel()}
cassandraApiClient={new CassandraAPIDataClient()}
/>
);
}
public openGitHubReposPanel(header: string, junoClient?: JunoClient): void { public openGitHubReposPanel(header: string, junoClient?: JunoClient): void {
this.openSidePanel( this.openSidePanel(
header, header,
@@ -132,6 +132,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
onChange={(event: React.ChangeEvent<HTMLInputElement>) => { onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
onLabelChange(event); onLabelChange(event);
}} }}
autoFocus
/> />
<div className="actionCol"></div> <div className="actionCol"></div>
</div> </div>
@@ -139,6 +139,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false); mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
mockExplorer.isShellEnabled = ko.observable(true);
}); });
afterAll(() => { afterAll(() => {
@@ -154,6 +155,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false); mockExplorer.isRunningOnNationalCloud = ko.observable(false);
mockExplorer.isShellEnabled = ko.observable(true);
}); });
it("Mongo Api not available - button should be hidden", () => { it("Mongo Api not available - button should be hidden", () => {
@@ -173,24 +175,18 @@ describe("CommandBarComponentButtonFactory tests", () => {
expect(openMongoShellBtn).toBeUndefined(); expect(openMongoShellBtn).toBeUndefined();
}); });
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => { it("Notebooks is not enabled and is unavailable - button should be hidden", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined(); expect(openMongoShellBtn).toBeUndefined();
expect(openMongoShellBtn.disabled).toBe(true);
expect(openMongoShellBtn.tooltipText).toBe(
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
);
}); });
it("Notebooks is not enabled and is available - button should be shown and enabled", () => { it("Notebooks is not enabled and is available - button should be hidden", () => {
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true); mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeDefined(); expect(openMongoShellBtn).toBeUndefined();
expect(openMongoShellBtn.disabled).toBe(false);
expect(openMongoShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => { it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
@@ -213,6 +209,16 @@ describe("CommandBarComponentButtonFactory tests", () => {
expect(openMongoShellBtn.disabled).toBe(false); expect(openMongoShellBtn.disabled).toBe(false);
expect(openMongoShellBtn.tooltipText).toBe(""); expect(openMongoShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is available, terminal is unavailable due to ipRules - button should be hidden", () => {
mockExplorer.isNotebookEnabled = ko.observable(true);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(true);
mockExplorer.isShellEnabled = ko.observable(false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
});
}); });
describe("Open Cassandra Shell button", () => { describe("Open Cassandra Shell button", () => {
@@ -273,11 +279,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => { it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined(); expect(openCassandraShellBtn).toBeUndefined();
expect(openCassandraShellBtn.disabled).toBe(true);
expect(openCassandraShellBtn.tooltipText).toBe(
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."
);
}); });
it("Notebooks is not enabled and is available - button should be shown and enabled", () => { it("Notebooks is not enabled and is available - button should be shown and enabled", () => {
@@ -285,9 +287,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeDefined(); expect(openCassandraShellBtn).toBeUndefined();
expect(openCassandraShellBtn.disabled).toBe(false);
expect(openCassandraShellBtn.tooltipText).toBe("");
}); });
it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => { it("Notebooks is enabled and is unavailable - button should be shown and enabled", () => {
@@ -61,48 +61,40 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
if (container.notebookManager?.gitHubOAuthService) { if (container.notebookManager?.gitHubOAuthService) {
buttons.push(createManageGitHubAccountButton(container)); buttons.push(createManageGitHubAccountButton(container));
} }
}
if (!container.isRunningOnNationalCloud()) {
if (!container.isNotebookEnabled()) {
buttons.push(createEnableNotebooksButton(container));
}
if (userContext.apiType === "Mongo") {
buttons.push(createOpenMongoTerminalButton(container));
}
if (userContext.apiType === "Cassandra") {
buttons.push(createOpenCassandraTerminalButton(container));
}
}
if (container.isNotebookEnabled()) {
buttons.push(createOpenTerminalButton(container)); buttons.push(createOpenTerminalButton(container));
buttons.push(createNotebookWorkspaceResetButton(container)); buttons.push(createNotebookWorkspaceResetButton(container));
if (
(userContext.apiType === "Mongo" && container.isShellEnabled() && container.isDatabaseNodeOrNoneSelected()) ||
userContext.apiType === "Cassandra"
) {
buttons.push(createDivider());
if (userContext.apiType === "Cassandra") {
buttons.push(createOpenCassandraTerminalButton(container));
} else {
buttons.push(createOpenMongoTerminalButton(container));
}
}
} else {
if (!container.isRunningOnNationalCloud()) {
buttons.push(createEnableNotebooksButton(container));
}
} }
if (!container.isDatabaseNodeOrNoneSelected()) { if (!container.isDatabaseNodeOrNoneSelected()) {
if (container.isNotebookEnabled()) { const isQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
buttons.push(createDivider());
}
const isSqlQuerySupported = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; if (isQuerySupported) {
if (isSqlQuerySupported) { buttons.push(createDivider());
const newSqlQueryBtn = createNewSQLQueryButton(container); const newSqlQueryBtn = createNewSQLQueryButton(container);
buttons.push(newSqlQueryBtn); buttons.push(newSqlQueryBtn);
} }
const isSupportedOpenQueryApi = if (isQuerySupported && container.selectedNode() && container.findSelectedCollection()) {
userContext.apiType === "SQL" || userContext.apiType === "Mongo" || userContext.apiType === "Gremlin";
const isSupportedOpenQueryFromDiskApi = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) {
const openQueryBtn = createOpenQueryButton(container); const openQueryBtn = createOpenQueryButton(container);
openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)]; openQueryBtn.children = [createOpenQueryButton(container), createOpenQueryFromDiskButton(container)];
buttons.push(openQueryBtn); buttons.push(openQueryBtn);
} else if (isSupportedOpenQueryFromDiskApi && container.selectedNode() && container.findSelectedCollection()) {
buttons.push(createOpenQueryFromDiskButton(container));
} }
if (areScriptsSupported()) { if (areScriptsSupported()) {
@@ -132,13 +124,17 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt
const buttons: CommandButtonComponentProps[] = []; const buttons: CommandButtonComponentProps[] = [];
if (!container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") { if (!container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label = "New Shell"; const label = container.isShellEnabled() ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = { const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onNewMongoShellClick(); if (container.isShellEnabled()) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
}, },
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
-25
View File
@@ -3,7 +3,6 @@ import { ActionContracts } from "../Contracts/ExplorerContracts";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "./Explorer"; import Explorer from "./Explorer";
import { handleOpenAction } from "./OpenActions"; import { handleOpenAction } from "./OpenActions";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
describe("OpenActions", () => { describe("OpenActions", () => {
describe("handleOpenAction", () => { describe("handleOpenAction", () => {
@@ -15,8 +14,6 @@ describe("OpenActions", () => {
beforeEach(() => { beforeEach(() => {
explorer = {} as Explorer; explorer = {} as Explorer;
explorer.onNewCollectionClicked = jest.fn(); explorer.onNewCollectionClicked = jest.fn();
explorer.cassandraAddCollectionPane = {} as CassandraAddCollectionPane;
explorer.cassandraAddCollectionPane.open = jest.fn();
database = { database = {
id: ko.observable("db"), id: ko.observable("db"),
@@ -64,28 +61,6 @@ describe("OpenActions", () => {
expect(actionHandled).toBe(true); expect(actionHandled).toBe(true);
}); });
describe("CassandraAddCollection pane kind", () => {
it("string value should call cassandraAddCollectionPane.open", () => {
const action = {
actionType: "OpenPane",
paneKind: "CassandraAddCollection",
};
const actionHandled = handleOpenAction(action, [], explorer);
expect(explorer.cassandraAddCollectionPane.open).toHaveBeenCalled();
});
it("enum value should call cassandraAddCollectionPane.open", () => {
const action = {
actionType: "OpenPane",
paneKind: ActionContracts.PaneKind.CassandraAddCollection,
};
const actionHandled = handleOpenAction(action, [], explorer);
expect(explorer.cassandraAddCollectionPane.open).toHaveBeenCalled();
});
});
describe("AddCollection pane kind", () => { describe("AddCollection pane kind", () => {
it("string value should call explorer.onNewCollectionClicked", () => { it("string value should call explorer.onNewCollectionClicked", () => {
const action = { const action = {
+1 -1
View File
@@ -145,7 +145,7 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
action.paneKind === ActionContracts.PaneKind.CassandraAddCollection || action.paneKind === ActionContracts.PaneKind.CassandraAddCollection ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection] (<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]
) { ) {
explorer.cassandraAddCollectionPane.open(); explorer.openCassandraAddCollectionPane();
} else if ( } else if (
action.paneKind === ActionContracts.PaneKind.GlobalSettings || action.paneKind === ActionContracts.PaneKind.GlobalSettings ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings] (<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings]
@@ -1,273 +0,0 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div
class="contextual-pane-out"
data-bind="
click: cancel,
clickBubble: false"
></div>
<div class="contextual-pane" id="cassandraaddcollectionpane">
<!-- Add Cassandra collection form - Start -->
<div class="contextual-pane-in">
<form
class="paneContentContainer"
role="dialog"
aria-label="Add Table"
data-bind="
submit: submit"
>
<!-- Add Cassandra collection header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="
click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Add Cassandra collection header - End -->
<!-- Add Cassandra collection errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
</span>
</div>
</div>
<!-- Add Cassandra collection errors - End -->
<div class="paneMainContent">
<div class="seconddivpadding">
<p>
<span class="mandatoryStar">*</span> Keyspace name
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext infoTooltipWidth"
>Select an existing keyspace or enter a new keyspace id.</span
>
</span>
</p>
<div class="createNewDatabaseOrUseExisting">
<input
class="createNewDatabaseOrUseExistingRadio"
aria-label="Create new keyspace"
name="databaseType"
type="radio"
role="radio"
id="keyspaceCreateNew"
data-test="addCollection-newDatabase"
tabindex="0"
data-bind="checked: keyspaceCreateNew, checkedValue: true, attr: { 'aria-checked': keyspaceCreateNew() ? 'true' : 'false' }"
/>
<span class="createNewDatabaseOrUseExistingSpace" for="keyspaceCreateNew">Create new</span>
<input
class="createNewDatabaseOrUseExistingRadio"
aria-label="Use existing keyspace"
name="databaseType"
type="radio"
role="radio"
id="keyspaceUseExisting"
data-test="addCollection-existingDatabase"
tabindex="0"
data-bind="checked: keyspaceCreateNew, checkedValue: false, attr: { 'aria-checked': !keyspaceCreateNew() ? 'true' : 'false' }"
/>
<span class="createNewDatabaseOrUseExistingSpace" for="keyspaceUseExisting">Use existing</span>
</div>
<input
id="keyspace-id"
data-test="addCollection-keyspaceId"
type="text"
autocomplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Type a new keyspace id"
size="40"
class="collid"
data-bind="visible: keyspaceCreateNew, textInput: keyspaceId, hasFocus: firstFieldHasFocus"
aria-label="Keyspace id"
aria-required="true"
autofocus
/>
<input
type="text"
aria-required="true"
autocomplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
list="keyspacesList"
placeholder="Choose existing keyspace id"
size="40"
class="collid"
data-bind="visible: !keyspaceCreateNew(), textInput: keyspaceId, hasFocus: firstFieldHasFocus"
aria-label="Keyspace id"
/>
<datalist id="keyspacesList" data-bind="foreach: container.databases">
<option data-bind="value: $data.id"></option>
</datalist>
<!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput -->
<div
class="databaseProvision"
aria-label="New database provision support"
data-bind="visible: keyspaceCreateNew"
>
<input
tabindex="0"
type="checkbox"
id="keyspaceSharedThroughput"
title="Provision shared throughput"
data-bind="checked: keyspaceHasSharedOffer"
/>
<span class="databaseProvisionText" for="keyspaceSharedThroughput">Provision keyspace throughput</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext provisionDatabaseThroughput"
>Provisioned throughput at the keyspace level will be shared across unlimited number of tables within
the keyspace</span
>
</span>
</div>
<!-- 1 -->
<div data-bind="visible: keyspaceCreateNew() && keyspaceHasSharedOffer()">
<throughput-input-autopilot-v3
params="{
testId: 'cassandraThroughputValue-v3-shared',
value: keyspaceThroughput,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: keyspaceCreateNew() && keyspaceHasSharedOffer(),
label: sharedThroughputRangeText,
ariaLabel: sharedThroughputRangeText,
requestUnitsUsageCost: requestUnitsUsageCostShared,
spendAckChecked: sharedThroughputSpendAck,
spendAckId: 'sharedThroughputSpendAck-v3-shared',
spendAckText: sharedThroughputSpendAckText,
spendAckVisible: sharedThroughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newKeyspace-databaseThroughput-autoPilotRadio-v3-shared',
throughputProvisionedRadioId: 'newKeyspace-databaseThroughput-manualRadio-v3-shared',
isAutoPilotSelected: isSharedAutoPilotSelected,
maxAutoPilotThroughputSet: sharedAutoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
costsVisible: costsVisible,
}"
>
</throughput-input-autopilot-v3>
</div>
<!-- /ko -->
<!-- Database provisioned throughput - End -->
</div>
<div class="seconddivpadding">
<p>
<span class="mandatoryStar">*</span> Enter CQL command to create the table.
<a href="https://aka.ms/cassandra-create-table" target="_blank">Learn More</a>
</p>
<div data-bind="text: createTableQuery" style="float: left; padding-top: 3px; padding-right: 3px"></div>
<input
type="text"
data-test="addCollection-tableId"
aria-required="true"
autocomplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
data-test="addCollection-tableId"
placeholder="Enter tableId"
size="20"
class="textfontclr"
data-bind="value: tableId"
style="margin-bottom: 5px"
/>
<textarea
id="editor-area"
rows="15"
aria-label="Table Schema"
data-bind="value: userTableQuery"
style="height: 125px; width: calc(100% - 80px); resize: vertical"
></textarea>
</div>
<!-- Provision table throughput - start -->
<!-- ko if: canConfigureThroughput -->
<div class="seconddivpadding" data-bind="visible: keyspaceHasSharedOffer() && !keyspaceCreateNew()">
<input
type="checkbox"
id="tableSharedThroughput"
title="Provision dedicated throughput for this table"
data-bind="checked: dedicateTableThroughput"
/>
<span for="tableSharedThroughput">Provision dedicated throughput for this table</span>
<span class="leftAlignInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="tooltiptext sharedCollectionThroughputTooltipWidth"
>You can optionally provision dedicated throughput for a table within a keyspace that has throughput
provisioned. This dedicated throughput amount will not be shared with other tables in the keyspace and
does not count towards the throughput you provisioned for the keyspace. This throughput amount will be
billed in addition to the throughput amount you provisioned at the keyspace level.</span
>
</span>
</div>
<!-- 2 -->
<div data-bind="visible: !keyspaceHasSharedOffer() || dedicateTableThroughput()">
<throughput-input-autopilot-v3
params="{
testId: 'cassandraSharedThroughputValue-v3-dedicated',
value: throughput,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: !keyspaceHasSharedOffer() || dedicateTableThroughput(),
label: throughputRangeText,
ariaLabel: throughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCostDedicated,
spendAckChecked: throughputSpendAck,
spendAckId: 'throughputSpendAckCassandra-v3-dedicated',
spendAckText: throughputSpendAckText,
spendAckVisible: throughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newKeyspace-containerThroughput-autoPilotRadio-v3-dedicated',
throughputProvisionedRadioId: 'newKeyspace-containerThroughput-manualRadio-v3-dedicated',
isAutoPilotSelected: isAutoPilotSelected,
maxAutoPilotThroughputSet: selectedAutoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
overrideWithAutoPilotSettings: false,
overrideWithProvisionedThroughputSettings: false
}"
>
</throughput-input-autopilot-v3>
</div>
<!-- /ko -->
<!-- Provision table throughput - end -->
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input type="submit" data-test="addCollection-createCollection" value="OK" class="btncreatecoll1" />
</div>
</div>
</form>
<!-- Add Cassandra collection form - End -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" alt="loading indicator" />
</div>
<!-- Loader - End -->
</div>
</div>
</div>
@@ -1,539 +0,0 @@
import * as ko from "knockout";
import * as _ from "underscore";
import * as Constants from "../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
import * as SharedConstants from "../../Shared/Constants";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../Utils/PricingUtils";
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ContextualPaneBase } from "./ContextualPaneBase";
export default class CassandraAddCollectionPane extends ContextualPaneBase {
public createTableQuery: ko.Observable<string>;
public keyspaceId: ko.Observable<string>;
public maxThroughputRU: ko.Observable<number>;
public minThroughputRU: ko.Observable<number>;
public tableId: ko.Observable<string>;
public throughput: ko.Observable<number>;
public throughputRangeText: ko.Computed<string>;
public sharedThroughputRangeText: ko.Computed<string>;
public userTableQuery: ko.Observable<string>;
public requestUnitsUsageCostDedicated: ko.Computed<string>;
public requestUnitsUsageCostShared: ko.Computed<string>;
public costsVisible: ko.PureComputed<boolean>;
public keyspaceHasSharedOffer: ko.Observable<boolean>;
public keyspaceIds: ko.ObservableArray<string>;
public keyspaceThroughput: ko.Observable<number>;
public keyspaceCreateNew: ko.Observable<boolean>;
public dedicateTableThroughput: ko.Observable<boolean>;
public canRequestSupport: ko.PureComputed<boolean>;
public throughputSpendAckText: ko.Observable<string>;
public throughputSpendAck: ko.Observable<boolean>;
public sharedThroughputSpendAck: ko.Observable<boolean>;
public sharedThroughputSpendAckText: ko.Observable<string>;
public isAutoPilotSelected: ko.Observable<boolean>;
public isSharedAutoPilotSelected: ko.Observable<boolean>;
public selectedAutoPilotThroughput: ko.Observable<number>;
public sharedAutoPilotThroughput: ko.Observable<number>;
public autoPilotUsageCost: ko.Computed<string>;
public sharedThroughputSpendAckVisible: ko.Computed<boolean>;
public throughputSpendAckVisible: ko.Computed<boolean>;
public canExceedMaximumValue: ko.PureComputed<boolean>;
public isFreeTierAccount: ko.Computed<boolean>;
public ruToolTipText: ko.Computed<string>;
public canConfigureThroughput: ko.PureComputed<boolean>;
private keyspaceOffers: Map<string, DataModels.Offer>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.title("Add Table");
this.createTableQuery = ko.observable<string>("CREATE TABLE ");
this.keyspaceCreateNew = ko.observable<boolean>(true);
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText());
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.keyspaceOffers = new Map();
this.keyspaceIds = ko.observableArray<string>();
this.keyspaceHasSharedOffer = ko.observable<boolean>(false);
this.keyspaceThroughput = ko.observable<number>();
this.keyspaceId = ko.observable<string>("");
this.keyspaceId.subscribe((keyspaceId: string) => {
if (this.keyspaceIds.indexOf(keyspaceId) >= 0) {
this.keyspaceHasSharedOffer(this.keyspaceOffers.has(keyspaceId));
}
});
this.keyspaceId.extend({ rateLimit: 100 });
this.dedicateTableThroughput = ko.observable<boolean>(false);
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
this.maxThroughputRU = ko.observable<number>(throughputDefaults.unlimitedmax);
this.minThroughputRU = ko.observable<number>(throughputDefaults.unlimitedmin);
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
this.isFreeTierAccount = ko.computed<boolean>(() => {
return userContext?.databaseAccount?.properties?.enableFreeTier;
});
this.tableId = ko.observable<string>("");
this.isAutoPilotSelected = ko.observable<boolean>(false);
this.isSharedAutoPilotSelected = ko.observable<boolean>(false);
this.selectedAutoPilotThroughput = ko.observable<number>();
this.sharedAutoPilotThroughput = ko.observable<number>();
this.throughput = ko.observable<number>();
this.throughputRangeText = ko.pureComputed<string>(() => {
const enableAutoPilot = this.isAutoPilotSelected();
if (!enableAutoPilot) {
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
}
return AutoPilotUtils.getAutoPilotHeaderText();
});
this.sharedThroughputRangeText = ko.pureComputed<string>(() => {
if (this.isSharedAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText();
}
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
});
this.userTableQuery = ko.observable<string>("(userid int, name text, email text, PRIMARY KEY (userid))");
this.keyspaceId.subscribe((keyspaceId) => {
this.createTableQuery(`CREATE TABLE ${keyspaceId}.`);
});
this.throughputSpendAckText = ko.observable<string>();
this.throughputSpendAck = ko.observable<boolean>(false);
this.sharedThroughputSpendAck = ko.observable<boolean>(false);
this.sharedThroughputSpendAckText = ko.observable<string>();
this.resetData();
this.requestUnitsUsageCostDedicated = ko.computed(() => {
const { databaseAccount: account } = userContext;
if (!account) {
return "";
}
const regions =
(account &&
account.properties &&
account.properties.readLocations &&
account.properties.readLocations.length) ||
1;
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
const offerThroughput: number = this.throughput();
let estimatedSpend: string;
let estimatedDedicatedSpendAcknowledge: string;
if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
userContext.portalEnv,
regions,
multimaster
);
estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput,
userContext.portalEnv,
regions,
multimaster,
this.isAutoPilotSelected()
);
} else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.selectedAutoPilotThroughput(),
userContext.portalEnv,
regions,
multimaster
);
estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.selectedAutoPilotThroughput(),
userContext.portalEnv,
regions,
multimaster,
this.isAutoPilotSelected()
);
}
this.throughputSpendAckText(estimatedDedicatedSpendAcknowledge);
return estimatedSpend;
});
this.requestUnitsUsageCostShared = ko.computed(() => {
const { databaseAccount: account } = userContext;
if (!account) {
return "";
}
const regions = account?.properties?.readLocations?.length || 1;
const multimaster = account?.properties?.enableMultipleWriteLocations || false;
let estimatedSpend: string;
let estimatedSharedSpendAcknowledge: string;
if (!this.isSharedAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
this.keyspaceThroughput(),
userContext.portalEnv,
regions,
multimaster
);
estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.keyspaceThroughput(),
userContext.portalEnv,
regions,
multimaster,
this.isSharedAutoPilotSelected()
);
} else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
this.sharedAutoPilotThroughput(),
userContext.portalEnv,
regions,
multimaster
);
estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.sharedAutoPilotThroughput(),
userContext.portalEnv,
regions,
multimaster,
this.isSharedAutoPilotSelected()
);
}
this.sharedThroughputSpendAckText(estimatedSharedSpendAcknowledge);
return estimatedSpend;
});
this.costsVisible = ko.pureComputed(() => {
return configContext.platform !== Platform.Emulator;
});
this.canRequestSupport = ko.pureComputed(() => {
if (configContext.platform !== Platform.Emulator && !userContext.isTryCosmosDBSubscription) {
const offerThroughput: number = this.throughput();
return offerThroughput <= 100000;
}
return false;
});
this.sharedThroughputSpendAckVisible = ko.computed<boolean>(() => {
const autoscaleThroughput = this.sharedAutoPilotThroughput() * 1;
if (this.isSharedAutoPilotSelected()) {
return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
}
return this.keyspaceThroughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
});
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
const autoscaleThroughput = this.selectedAutoPilotThroughput() * 1;
if (this.isAutoPilotSelected()) {
return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
}
return this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
});
if (!!this.container) {
const updateKeyspaceIds: (keyspaces: ViewModels.Database[]) => void = (
newKeyspaceIds: ViewModels.Database[]
): void => {
const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => {
if (keyspace && keyspace.offer && !!keyspace.offer()) {
this.keyspaceOffers.set(keyspace.id(), keyspace.offer());
}
return keyspace.id();
});
this.keyspaceIds(cachedKeyspaceIdsList);
};
this.container.databases.subscribe((newDatabases: ViewModels.Database[]) => updateKeyspaceIds(newDatabases));
updateKeyspaceIds(this.container.databases());
}
this.autoPilotUsageCost = ko.pureComputed<string>(() => {
const autoPilot = this._getAutoPilot();
if (!autoPilot) {
return "";
}
const isDatabaseThroughput: boolean = this.keyspaceCreateNew();
return PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, isDatabaseThroughput);
});
}
public decreaseThroughput() {
let offerThroughput: number = this.throughput();
if (offerThroughput > this.minThroughputRU()) {
offerThroughput -= 100;
this.throughput(offerThroughput);
}
}
public increaseThroughput() {
let offerThroughput: number = this.throughput();
if (offerThroughput < this.maxThroughputRU()) {
offerThroughput += 100;
this.throughput(offerThroughput);
}
}
public open() {
super.open();
if (!this.container.isServerlessEnabled()) {
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
}
const addCollectionPaneOpenMessage = {
collection: ko.toJS({
id: this.tableId(),
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
offerThroughput: this.throughput(),
partitionKey: "",
databaseId: this.keyspaceId(),
}),
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
const focusElement = document.getElementById("keyspace-id");
focusElement && focusElement.focus();
TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage);
}
public submit() {
if (!this._isValid()) {
return;
}
this.isExecuting(true);
const autoPilotCommand = `cosmosdb_autoscale_max_throughput`;
let createTableAndKeyspacePromise: Q.Promise<any>;
const toCreateKeyspace: boolean = this.keyspaceCreateNew();
const useAutoPilotForKeyspace: boolean = this.isSharedAutoPilotSelected() && !!this.sharedAutoPilotThroughput();
const createKeyspaceQueryPrefix: string = `CREATE KEYSPACE ${this.keyspaceId().trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }`;
const createKeyspaceQuery: string = this.keyspaceHasSharedOffer()
? useAutoPilotForKeyspace
? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${this.sharedAutoPilotThroughput()};`
: `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${this.keyspaceThroughput()};`
: `${createKeyspaceQueryPrefix};`;
const createTableQueryPrefix: string = `${this.createTableQuery()}${this.tableId().trim()} ${this.userTableQuery()}`;
let createTableQuery: string;
if (this.canConfigureThroughput() && (this.dedicateTableThroughput() || !this.keyspaceHasSharedOffer())) {
if (this.isAutoPilotSelected() && this.selectedAutoPilotThroughput()) {
createTableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${this.selectedAutoPilotThroughput()};`;
} else {
createTableQuery = `${createTableQueryPrefix} WITH cosmosdb_provisioned_throughput=${this.throughput()};`;
}
} else {
createTableQuery = `${createTableQueryPrefix};`;
}
const addCollectionPaneStartMessage = {
collection: ko.toJS({
id: this.tableId(),
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
offerThroughput: this.throughput(),
partitionKey: "",
databaseId: this.keyspaceId(),
hasDedicatedThroughput: this.dedicateTableThroughput(),
}),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace,
createKeyspaceQuery: createKeyspaceQuery,
createTableQuery: createTableQuery,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, addCollectionPaneStartMessage);
const { databaseAccount } = userContext;
if (toCreateKeyspace) {
createTableAndKeyspacePromise = (<CassandraAPIDataClient>this.container.tableDataClient).createTableAndKeyspace(
databaseAccount?.properties.cassandraEndpoint,
databaseAccount?.id,
this.container,
createTableQuery,
createKeyspaceQuery
);
} else {
createTableAndKeyspacePromise = (<CassandraAPIDataClient>this.container.tableDataClient).createTableAndKeyspace(
databaseAccount?.properties.cassandraEndpoint,
databaseAccount?.id,
this.container,
createTableQuery
);
}
createTableAndKeyspacePromise.then(
() => {
this.container.refreshAllDatabases();
this.isExecuting(false);
this.close();
const addCollectionPaneSuccessMessage = {
collection: ko.toJS({
id: this.tableId(),
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
offerThroughput: this.throughput(),
partitionKey: "",
databaseId: this.keyspaceId(),
hasDedicatedThroughput: this.dedicateTableThroughput(),
}),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace,
createKeyspaceQuery: createKeyspaceQuery,
createTableQuery: createTableQuery,
};
TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneSuccessMessage, startKey);
},
(error) => {
const errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.isExecuting(false);
const addCollectionPaneFailedMessage = {
collection: {
id: this.tableId(),
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
offerThroughput: this.throughput(),
partitionKey: "",
databaseId: this.keyspaceId(),
hasDedicatedThroughput: this.dedicateTableThroughput(),
},
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: "u",
throughput: this.throughput(),
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
toCreateKeyspace: toCreateKeyspace,
createKeyspaceQuery: createKeyspaceQuery,
createTableQuery: createTableQuery,
error: errorMessage,
errorStack: getErrorStack(error),
};
TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey);
}
);
}
public resetData() {
super.resetData();
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
this.isAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
this.isSharedAutoPilotSelected(this.container.isAutoscaleDefaultEnabled());
this.selectedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.throughput(AddCollectionUtility.getMaxThroughput(this.container.collectionCreationDefaults, this.container));
this.keyspaceThroughput(throughputDefaults.shared);
this.maxThroughputRU(throughputDefaults.unlimitedmax);
this.minThroughputRU(throughputDefaults.unlimitedmin);
this.createTableQuery("CREATE TABLE ");
this.userTableQuery("(userid int, name text, email text, PRIMARY KEY (userid))");
this.tableId("");
this.keyspaceId("");
this.throughputSpendAck(false);
this.keyspaceHasSharedOffer(false);
this.keyspaceCreateNew(true);
}
private _isValid(): boolean {
const throughput = this.throughput();
const keyspaceThroughput = this.keyspaceThroughput();
const sharedAutoscaleThroughput = this.sharedAutoPilotThroughput() * 1;
if (
this.isSharedAutoPilotSelected() &&
sharedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.sharedThroughputSpendAck()
) {
this.formErrors(`Please acknowledge the estimated monthly spend.`);
return false;
}
const dedicatedAutoscaleThroughput = this.selectedAutoPilotThroughput() * 1;
if (
this.isAutoPilotSelected() &&
dedicatedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.throughputSpendAck()
) {
this.formErrors(`Please acknowledge the estimated monthly spend.`);
return false;
}
if (
(this.keyspaceCreateNew() && this.keyspaceHasSharedOffer() && this.isSharedAutoPilotSelected()) ||
this.isAutoPilotSelected()
) {
const autoPilot = this._getAutoPilot();
if (
!autoPilot ||
!autoPilot.maxThroughput ||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
) {
this.formErrors(
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
);
return false;
}
return true;
}
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) {
this.formErrors(`Please acknowledge the estimated daily spend.`);
return false;
}
if (
this.keyspaceHasSharedOffer() &&
this.keyspaceCreateNew() &&
keyspaceThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.sharedThroughputSpendAck()
) {
this.formErrors("Please acknowledge the estimated daily spend");
return false;
}
return true;
}
private _getAutoPilot(): DataModels.AutoPilotCreationSettings {
if (
this.keyspaceCreateNew() &&
this.keyspaceHasSharedOffer() &&
this.isSharedAutoPilotSelected() &&
this.sharedAutoPilotThroughput()
) {
return {
maxThroughput: this.sharedAutoPilotThroughput() * 1,
};
}
if (this.selectedAutoPilotThroughput()) {
return {
maxThroughput: this.selectedAutoPilotThroughput() * 1,
};
}
return undefined;
}
}
@@ -0,0 +1,32 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { shallow } from "enzyme";
import React from "react";
import Explorer from "../../Explorer";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
import { CassandraAddCollectionPane } from "./CassandraAddCollectionPane";
const props = {
explorer: new Explorer(),
closePanel: (): void => undefined,
cassandraApiClient: new CassandraAPIDataClient(),
};
describe("CassandraAddCollectionPane Pane", () => {
beforeEach(() => render(<CassandraAddCollectionPane {...props} />));
it("should render Default properly", () => {
const wrapper = shallow(<CassandraAddCollectionPane {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("click on is Create new keyspace", () => {
fireEvent.click(screen.getByLabelText("Create new keyspace"));
expect(screen.getByLabelText("Provision keyspace throughput")).toBeDefined();
});
it("click on Use existing", () => {
fireEvent.click(screen.getByLabelText("Use existing keyspace"));
});
it("Enter Keyspace name ", () => {
fireEvent.change(screen.getByLabelText("Keyspace id"), { target: { value: "unittest1" } });
expect(screen.getByLabelText("CREATE TABLE unittest1.")).toBeDefined();
});
});
@@ -0,0 +1,427 @@
import { Label, Stack, TextField } from "@fluentui/react";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore";
import * as Constants from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as AddCollectionUtility from "../../../Shared/AddCollectionUtility";
import * as SharedConstants from "../../../Shared/Constants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export interface CassandraAddCollectionPaneProps {
explorer: Explorer;
closePanel: () => void;
cassandraApiClient: CassandraAPIDataClient;
}
export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectionPaneProps> = ({
explorer: container,
closePanel,
cassandraApiClient,
}: CassandraAddCollectionPaneProps) => {
const throughputDefaults = container.collectionCreationDefaults.throughput;
const [createTableQuery, setCreateTableQuery] = useState<string>("CREATE TABLE ");
const [keyspaceId, setKeyspaceId] = useState<string>("");
const [tableId, setTableId] = useState<string>("");
const [throughput, setThroughput] = useState<number>(
AddCollectionUtility.getMaxThroughput(container.collectionCreationDefaults, container)
);
const [isAutoPilotSelected, setIsAutoPilotSelected] = useState<boolean>(container.isAutoscaleDefaultEnabled());
const [isSharedAutoPilotSelected, setIsSharedAutoPilotSelected] = useState<boolean>(
container.isAutoscaleDefaultEnabled()
);
const [userTableQuery, setUserTableQuery] = useState<string>(
"(userid int, name text, email text, PRIMARY KEY (userid))"
);
const [keyspaceHasSharedOffer, setKeyspaceHasSharedOffer] = useState<boolean>(false);
const [keyspaceIds, setKeyspaceIds] = useState<string[]>([]);
const [keyspaceThroughput, setKeyspaceThroughput] = useState<number>(throughputDefaults.shared);
const [keyspaceCreateNew, setKeyspaceCreateNew] = useState<boolean>(true);
const [dedicateTableThroughput, setDedicateTableThroughput] = useState<boolean>(false);
const [throughputSpendAck, setThroughputSpendAck] = useState<boolean>(false);
const [sharedThroughputSpendAck, setSharedThroughputSpendAck] = useState<boolean>(false);
const { minAutoPilotThroughput: selectedAutoPilotThroughput } = AutoPilotUtils;
const { minAutoPilotThroughput: sharedAutoPilotThroughput } = AutoPilotUtils;
const _getAutoPilot = (): DataModels.AutoPilotCreationSettings => {
if (keyspaceCreateNew && keyspaceHasSharedOffer && isSharedAutoPilotSelected && sharedAutoPilotThroughput) {
return {
maxThroughput: sharedAutoPilotThroughput * 1,
};
}
if (selectedAutoPilotThroughput) {
return {
maxThroughput: selectedAutoPilotThroughput * 1,
};
}
return undefined;
};
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
const canConfigureThroughput = !container.isServerlessEnabled();
const keyspaceOffers = new Map();
const [isExecuting, setIsExecuting] = useState<boolean>();
const [formErrors, setFormErrors] = useState<string>("");
useEffect(() => {
if (keyspaceIds.indexOf(keyspaceId) >= 0) {
setKeyspaceHasSharedOffer(keyspaceOffers.has(keyspaceId));
}
setCreateTableQuery(`CREATE TABLE ${keyspaceId}.`);
}, [keyspaceId]);
const addCollectionPaneOpenMessage = {
collection: {
id: tableId,
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
offerThroughput: throughput,
partitionKey: "",
databaseId: keyspaceId,
},
subscriptionType: userContext.subscriptionType,
subscriptionQuotaId: userContext.quotaId,
defaultsCheck: {
storage: "u",
throughput,
flight: userContext.addCollectionFlight,
},
dataExplorerArea: Constants.Areas.ContextualPane,
};
useEffect(() => {
if (!container.isServerlessEnabled()) {
setIsAutoPilotSelected(container.isAutoscaleDefaultEnabled());
}
TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage);
}, []);
useEffect(() => {
if (container) {
const newKeyspaceIds: ViewModels.Database[] = container.databases();
const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => {
if (keyspace && keyspace.offer && !!keyspace.offer()) {
keyspaceOffers.set(keyspace.id(), keyspace.offer());
}
return keyspace.id();
});
setKeyspaceIds(cachedKeyspaceIdsList);
}
}, []);
const _isValid = () => {
const sharedAutoscaleThroughput = sharedAutoPilotThroughput * 1;
if (
isSharedAutoPilotSelected &&
sharedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!sharedThroughputSpendAck
) {
setFormErrors(`Please acknowledge the estimated monthly spend.`);
return false;
}
const dedicatedAutoscaleThroughput = selectedAutoPilotThroughput * 1;
if (
isAutoPilotSelected &&
dedicatedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!throughputSpendAck
) {
setFormErrors(`Please acknowledge the estimated monthly spend.`);
return false;
}
if ((keyspaceCreateNew && keyspaceHasSharedOffer && isSharedAutoPilotSelected) || isAutoPilotSelected) {
const autoPilot = _getAutoPilot();
if (
!autoPilot ||
!autoPilot.maxThroughput ||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
) {
setFormErrors(
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
);
return false;
}
return true;
}
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !throughputSpendAck) {
setFormErrors(`Please acknowledge the estimated daily spend.`);
return false;
}
if (
keyspaceHasSharedOffer &&
keyspaceCreateNew &&
keyspaceThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!sharedThroughputSpendAck
) {
setFormErrors("Please acknowledge the estimated daily spend");
return false;
}
return true;
};
const onSubmit = async () => {
if (!_isValid()) {
return;
}
setIsExecuting(true);
const autoPilotCommand = `cosmosdb_autoscale_max_throughput`;
const toCreateKeyspace: boolean = keyspaceCreateNew;
const useAutoPilotForKeyspace: boolean = isSharedAutoPilotSelected && !!sharedAutoPilotThroughput;
const createKeyspaceQueryPrefix = `CREATE KEYSPACE ${keyspaceId.trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }`;
const createKeyspaceQuery: string = keyspaceHasSharedOffer
? useAutoPilotForKeyspace
? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${keyspaceThroughput};`
: `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${keyspaceThroughput};`
: `${createKeyspaceQueryPrefix};`;
let tableQuery: string;
const createTableQueryPrefix = `${createTableQuery}${tableId.trim()} ${userTableQuery}`;
if (canConfigureThroughput && (dedicateTableThroughput || !keyspaceHasSharedOffer)) {
if (isAutoPilotSelected && selectedAutoPilotThroughput) {
tableQuery = `${createTableQueryPrefix} WITH ${autoPilotCommand}=${throughput};`;
} else {
tableQuery = `${createTableQueryPrefix} WITH cosmosdb_provisioned_throughput=${throughput};`;
}
} else {
tableQuery = `${createTableQueryPrefix};`;
}
const addCollectionPaneStartMessage = {
...addCollectionPaneOpenMessage,
collection: {
...addCollectionPaneOpenMessage.collection,
hasDedicatedThroughput: dedicateTableThroughput,
},
keyspaceHasSharedOffer,
toCreateKeyspace,
createKeyspaceQuery,
createTableQuery: tableQuery,
};
const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, addCollectionPaneStartMessage);
try {
if (toCreateKeyspace) {
await cassandraApiClient.createTableAndKeyspace(
userContext?.databaseAccount?.properties?.cassandraEndpoint,
userContext?.databaseAccount?.id,
container,
tableQuery,
createKeyspaceQuery
);
} else {
await cassandraApiClient.createTableAndKeyspace(
userContext?.databaseAccount?.properties?.cassandraEndpoint,
userContext?.databaseAccount?.id,
container,
tableQuery
);
}
container.refreshAllDatabases();
setIsExecuting(false);
closePanel();
TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneStartMessage, startKey);
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormErrors(errorMessage);
setIsExecuting(false);
const addCollectionPaneFailedMessage = {
...addCollectionPaneStartMessage,
error: errorMessage,
errorStack: getErrorStack(error),
};
TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey);
}
};
const handleOnChangeKeyspaceType = (ev: React.FormEvent<HTMLInputElement>, mode: string): void => {
setKeyspaceCreateNew(mode === "Create new");
};
const props: RightPaneFormProps = {
expandConsole: () => container.expandConsole(),
formError: formErrors,
isExecuting,
submitButtonText: "Apply",
onSubmit,
};
return (
<RightPaneForm {...props}>
<div className="paneMainContent">
<div className="seconddivpadding">
<p>
<Label required>
Keyspace name <InfoTooltip>Select an existing keyspace or enter a new keyspace id.</InfoTooltip>
</Label>
</p>
<Stack horizontal verticalAlign="center">
<input
className="throughputInputRadioBtn"
aria-label="Create new keyspace"
checked={keyspaceCreateNew}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeKeyspaceType(e, "Create new")}
/>
<span className="throughputInputRadioBtnLabel">Create new</span>
<input
className="throughputInputRadioBtn"
aria-label="Use existing keyspace"
checked={!keyspaceCreateNew}
type="radio"
role="radio"
tabIndex={0}
onChange={(e) => handleOnChangeKeyspaceType(e, "Use existing")}
/>
<span className="throughputInputRadioBtnLabel">Use existing</span>
</Stack>
<TextField
aria-required="true"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
list={keyspaceCreateNew ? "" : "keyspacesList"}
placeholder={keyspaceCreateNew ? "Type a new keyspace id" : "Choose existing keyspace id"}
size={40}
data-test="addCollection-keyspaceId"
value={keyspaceId}
onChange={(e, newValue) => setKeyspaceId(newValue)}
ariaLabel="Keyspace id"
autoFocus
/>
<datalist id="keyspacesList">
{keyspaceIds?.map((id: string, index: number) => (
<option key={index}>{id}</option>
))}
</datalist>
{canConfigureThroughput && keyspaceCreateNew && (
<div className="databaseProvision">
<input
tabIndex={0}
type="checkbox"
id="keyspaceSharedThroughput"
title="Provision shared throughput"
checked={keyspaceHasSharedOffer}
onChange={(e) => setKeyspaceHasSharedOffer(e.target.checked)}
/>
<span className="databaseProvisionText" aria-label="Provision keyspace throughput">
Provision keyspace throughput
</span>
<InfoTooltip>
Provisioned throughput at the keyspace level will be shared across unlimited number of tables within the
keyspace
</InfoTooltip>
</div>
)}
{canConfigureThroughput && keyspaceCreateNew && keyspaceHasSharedOffer && (
<div>
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container.isFirstResourceCreated()}
isDatabase
isSharded
setThroughputValue={(throughput: number) => setKeyspaceThroughput(throughput)}
setIsAutoscale={(isAutoscale: boolean) => setIsSharedAutoPilotSelected(isAutoscale)}
onCostAcknowledgeChange={(isAcknowledge: boolean) => {
setSharedThroughputSpendAck(isAcknowledge);
}}
/>
</div>
)}
</div>
<div className="seconddivpadding">
<p>
<Label required>
Enter CQL command to create the table.
<a href="https://aka.ms/cassandra-create-table" target="_blank" rel="noreferrer">
Learn More
</a>
</Label>
</p>
<div aria-label={createTableQuery} style={{ float: "left", paddingTop: "3px", paddingRight: "3px" }}>
{createTableQuery}
</div>
<TextField
aria-required="true"
ariaLabel="addCollection-tableId"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
placeholder="Enter tableId"
size={20}
className="textfontclr"
value={tableId}
onChange={(e, newValue) => setTableId(newValue)}
style={{ marginBottom: "5px" }}
/>
<TextField
multiline
id="editor-area"
rows={5}
aria-label="Table Schema"
value={userTableQuery}
onChange={(e, newValue) => setUserTableQuery(newValue)}
/>
</div>
{canConfigureThroughput && keyspaceHasSharedOffer && !keyspaceCreateNew && (
<div className="seconddivpadding">
<input
type="checkbox"
id="tableSharedThroughput"
title="Provision dedicated throughput for this table"
checked={dedicateTableThroughput}
onChange={(e) => setDedicateTableThroughput(e.target.checked)}
/>
<span>Provision dedicated throughput for this table</span>
<InfoTooltip>
You can optionally provision dedicated throughput for a table within a keyspace that has throughput
provisioned. This dedicated throughput amount will not be shared with other tables in the keyspace and
does not count towards the throughput you provisioned for the keyspace. This throughput amount will be
billed in addition to the throughput amount you provisioned at the keyspace level.
</InfoTooltip>
</div>
)}
{canConfigureThroughput && (!keyspaceHasSharedOffer || dedicateTableThroughput) && (
<div>
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container.isFirstResourceCreated()}
isDatabase={false}
isSharded={false}
setThroughputValue={(throughput: number) => setThroughput(throughput)}
setIsAutoscale={(isAutoscale: boolean) => setIsAutoPilotSelected(isAutoscale)}
onCostAcknowledgeChange={(isAcknowledge: boolean) => {
setThroughputSpendAck(isAcknowledge);
}}
/>
</div>
)}
</div>
</RightPaneForm>
);
};
@@ -0,0 +1,164 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CassandraAddCollectionPane Pane should render Default properly 1`] = `
<RightPaneForm
expandConsole={[Function]}
formError=""
onSubmit={[Function]}
submitButtonText="Apply"
>
<div
className="paneMainContent"
>
<div
className="seconddivpadding"
>
<p>
<StyledLabelBase
required={true}
>
Keyspace name
<InfoTooltip>
Select an existing keyspace or enter a new keyspace id.
</InfoTooltip>
</StyledLabelBase>
</p>
<Stack
horizontal={true}
verticalAlign="center"
>
<input
aria-label="Create new keyspace"
checked={true}
className="throughputInputRadioBtn"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="throughputInputRadioBtnLabel"
>
Create new
</span>
<input
aria-label="Use existing keyspace"
checked={false}
className="throughputInputRadioBtn"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="throughputInputRadioBtnLabel"
>
Use existing
</span>
</Stack>
<StyledTextFieldBase
aria-required="true"
ariaLabel="Keyspace id"
autoComplete="off"
autoFocus={true}
data-test="addCollection-keyspaceId"
list=""
onChange={[Function]}
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"
placeholder="Type a new keyspace id"
size={40}
title="May not end with space nor contain characters '\\\\' '/' '#' '?'"
value=""
/>
<datalist
id="keyspacesList"
/>
<div
className="databaseProvision"
>
<input
checked={false}
id="keyspaceSharedThroughput"
onChange={[Function]}
tabIndex={0}
title="Provision shared throughput"
type="checkbox"
/>
<span
aria-label="Provision keyspace throughput"
className="databaseProvisionText"
>
Provision keyspace throughput
</span>
<InfoTooltip>
Provisioned throughput at the keyspace level will be shared across unlimited number of tables within the keyspace
</InfoTooltip>
</div>
</div>
<div
className="seconddivpadding"
>
<p>
<StyledLabelBase
required={true}
>
Enter CQL command to create the table.
<a
href="https://aka.ms/cassandra-create-table"
rel="noreferrer"
target="_blank"
>
Learn More
</a>
</StyledLabelBase>
</p>
<div
aria-label="CREATE TABLE "
style={
Object {
"float": "left",
"paddingRight": "3px",
"paddingTop": "3px",
}
}
>
CREATE TABLE
</div>
<StyledTextFieldBase
aria-required="true"
ariaLabel="addCollection-tableId"
autoComplete="off"
className="textfontclr"
onChange={[Function]}
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"
placeholder="Enter tableId"
size={20}
style={
Object {
"marginBottom": "5px",
}
}
title="May not end with space nor contain characters '\\\\' '/' '#' '?'"
value=""
/>
<StyledTextFieldBase
aria-label="Table Schema"
id="editor-area"
multiline={true}
onChange={[Function]}
rows={5}
value="(userid int, name text, email text, PRIMARY KEY (userid))"
/>
</div>
<div>
<ThroughputInput
isDatabase={false}
isSharded={false}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setThroughputValue={[Function]}
/>
</div>
</div>
</RightPaneForm>
`;
@@ -9,10 +9,7 @@ import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUti
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
import { import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
interface Location { interface Location {
@@ -42,7 +39,6 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
}: CopyNotebookPanelProps) => { }: CopyNotebookPanelProps) => {
const [isExecuting, setIsExecuting] = useState<boolean>(); const [isExecuting, setIsExecuting] = useState<boolean>();
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [formErrorDetail, setFormErrorDetail] = useState<string>("");
const [pinnedRepos, setPinnedRepos] = useState<IPinnedRepo[]>(); const [pinnedRepos, setPinnedRepos] = useState<IPinnedRepo[]>();
const [selectedLocation, setSelectedLocation] = useState<Location>(); const [selectedLocation, setSelectedLocation] = useState<Location>();
@@ -92,7 +88,6 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
} catch (error) { } catch (error) {
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
setFormError(`Failed to copy ${name} to ${destination}`); setFormError(`Failed to copy ${name} to ${destination}`);
setFormErrorDetail(`${errorMessage}`);
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError); handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError);
} finally { } finally {
clearMessage && clearMessage(); clearMessage && clearMessage();
@@ -130,14 +125,10 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
setSelectedLocation(option?.data); setSelectedLocation(option?.data);
}; };
const genericPaneProps: GenericRightPaneProps = { const props: RightPaneFormProps = {
formError, formError,
formErrorDetail,
id: "copynotebookpane",
isExecuting: isExecuting, isExecuting: isExecuting,
title: "Copy notebook",
submitButtonText: "OK", submitButtonText: "OK",
onClose: closePanel,
onSubmit: () => submit(), onSubmit: () => submit(),
expandConsole: () => container.expandConsole(), expandConsole: () => container.expandConsole(),
}; };
@@ -149,8 +140,8 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
}; };
return ( return (
<GenericRightPaneComponent {...genericPaneProps}> <RightPaneForm {...props}>
<CopyNotebookPaneComponent {...copyNotebookPaneProps} /> <CopyNotebookPaneComponent {...copyNotebookPaneProps} />
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };
@@ -130,8 +130,8 @@ describe("Delete Collection Confirmation Pane", () => {
.hostNodes() .hostNodes()
.simulate("change", { target: { value: selectedCollectionId } }); .simulate("change", { target: { value: selectedCollectionId } });
expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true); expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click"); wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId); expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
wrapper.unmount(); wrapper.unmount();
@@ -151,8 +151,8 @@ describe("Delete Collection Confirmation Pane", () => {
.hostNodes() .hostNodes()
.simulate("change", { target: { value: feedbackText } }); .simulate("change", { target: { value: feedbackText } });
expect(wrapper.exists(".genericPaneSubmitBtn")).toBe(true); expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
wrapper.find(".genericPaneSubmitBtn").hostNodes().simulate("click"); wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit");
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId); expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
const deleteFeedback = new DeleteFeedback( const deleteFeedback = new DeleteFeedback(
@@ -12,10 +12,7 @@ import { userContext } from "../../../UserContext";
import { getCollectionName } from "../../../Utils/APITypeUtils"; import { getCollectionName } from "../../../Utils/APITypeUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
export interface DeleteCollectionConfirmationPaneProps { export interface DeleteCollectionConfirmationPaneProps {
explorer: Explorer; explorer: Explorer;
closePanel: () => void; closePanel: () => void;
@@ -35,7 +32,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
}; };
const collectionName = getCollectionName().toLocaleLowerCase(); const collectionName = getCollectionName().toLocaleLowerCase();
const paneTitle = "Delete " + collectionName; const paneTitle = "Delete " + collectionName;
const submit = async (): Promise<void> => { const onSubmit = async (): Promise<void> => {
const collection = explorer.findSelectedCollection(); const collection = explorer.findSelectedCollection();
if (!collection || inputCollectionName !== collection.id()) { if (!collection || inputCollectionName !== collection.id()) {
const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName; const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName;
@@ -100,19 +97,15 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
); );
} }
}; };
const genericPaneProps: GenericRightPaneProps = { const props: RightPaneFormProps = {
formError: formError, formError: formError,
formErrorDetail: formError,
id: "deleteCollectionpane",
isExecuting, isExecuting,
title: paneTitle,
submitButtonText: "OK", submitButtonText: "OK",
onClose: closePanel, onSubmit,
onSubmit: submit,
expandConsole: () => explorer.expandConsole(), expandConsole: () => explorer.expandConsole(),
}; };
return ( return (
<GenericRightPaneComponent {...genericPaneProps}> <RightPaneForm {...props}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">
<div className="panelMainContent"> <div className="panelMainContent">
<div className="confirmDeleteInput"> <div className="confirmDeleteInput">
@@ -150,6 +143,6 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
)} )}
</div> </div>
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };
@@ -98,8 +98,8 @@ describe("Delete Database Confirmation Pane", () => {
.find("#confirmDatabaseId") .find("#confirmDatabaseId")
.hostNodes() .hostNodes()
.simulate("change", { target: { value: selectedDatabaseId } }); .simulate("change", { target: { value: selectedDatabaseId } });
expect(wrapper.exists("#sidePanelOkButton")).toBe(true); expect(wrapper.exists("button")).toBe(true);
wrapper.find("#sidePanelOkButton").hostNodes().simulate("submit"); wrapper.find("button").hostNodes().simulate("submit");
expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId); expect(deleteDatabase).toHaveBeenCalledWith(selectedDatabaseId);
wrapper.unmount(); wrapper.unmount();
}); });
@@ -1,5 +1,5 @@
import { useBoolean } from "@fluentui/react-hooks";
import { Text, TextField } from "@fluentui/react"; import { Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import { Areas } from "../../Common/Constants"; import { Areas } from "../../Common/Constants";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase"; import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
@@ -12,9 +12,8 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { PanelFooterComponent } from "./PanelFooterComponent";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent"; import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { PanelLoadingScreen } from "./PanelLoadingScreen"; import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm";
interface DeleteDatabaseConfirmationPanelProps { interface DeleteDatabaseConfirmationPanelProps {
explorer: Explorer; explorer: Explorer;
@@ -23,36 +22,19 @@ interface DeleteDatabaseConfirmationPanelProps {
selectedDatabase: Database; selectedDatabase: Database;
} }
export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ( export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseConfirmationPanelProps> = ({
props: DeleteDatabaseConfirmationPanelProps explorer,
): JSX.Element => { openNotificationConsole,
closePanel,
selectedDatabase,
}: DeleteDatabaseConfirmationPanelProps): JSX.Element => {
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false); const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [databaseInput, setDatabaseInput] = useState<string>(""); const [databaseInput, setDatabaseInput] = useState<string>("");
const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState<string>(""); const [databaseFeedbackInput, setDatabaseFeedbackInput] = useState<string>("");
const getPanelErrorProps = (): PanelInfoErrorProps => { const submit = async (): Promise<void> => {
if (formError) {
return {
messageType: "error",
message: formError,
showErrorDetails: true,
openNotificationConsole: props.openNotificationConsole,
};
}
return {
messageType: "warning",
showErrorDetails: false,
message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
};
};
const submit = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
const { selectedDatabase, explorer } = props;
event.preventDefault();
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) { if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
setFormError("Input database name does not match the selected database"); setFormError("Input database name does not match the selected database");
logConsoleError(`Error while deleting collection ${selectedDatabase && selectedDatabase.id()}`); logConsoleError(`Error while deleting collection ${selectedDatabase && selectedDatabase.id()}`);
@@ -69,7 +51,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
try { try {
await deleteDatabase(selectedDatabase.id()); await deleteDatabase(selectedDatabase.id());
props.closePanel(); closePanel();
explorer.refreshAllDatabases(); explorer.refreshAllDatabases();
explorer.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id()); explorer.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
explorer.selectedNode(undefined); explorer.selectedNode(undefined);
@@ -121,13 +103,27 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
}; };
const shouldRecordFeedback = (): boolean => { const shouldRecordFeedback = (): boolean => {
const { explorer } = props;
return explorer.isLastNonEmptyDatabase() || (explorer.isLastDatabase() && explorer.isSelectedDatabaseShared()); return explorer.isLastNonEmptyDatabase() || (explorer.isLastDatabase() && explorer.isSelectedDatabaseShared());
}; };
const props: RightPaneFormProps = {
formError,
isExecuting: isLoading,
submitButtonText: "OK",
onSubmit: () => submit(),
expandConsole: openNotificationConsole,
};
const errorProps: PanelInfoErrorProps = {
messageType: "warning",
showErrorDetails: false,
message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
};
return ( return (
<form className="panelFormWrapper" onSubmit={submit}> <RightPaneForm {...props}>
<PanelInfoErrorComponent {...getPanelErrorProps()} /> {!formError && <PanelInfoErrorComponent {...errorProps} />}
<div className="panelMainContent"> <div className="panelMainContent">
<div className="confirmDeleteInput"> <div className="confirmDeleteInput">
<span className="mandatoryStar">* </span> <span className="mandatoryStar">* </span>
@@ -161,8 +157,6 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
</div> </div>
)} )}
</div> </div>
<PanelFooterComponent buttonLabel="OK" /> </RightPaneForm>
{isLoading && <PanelLoadingScreen />}
</form>
); );
}; };
@@ -1,12 +1,10 @@
import { useBoolean } from "@fluentui/react-hooks";
import { IDropdownOption, IImageProps, Image, Stack, Text } from "@fluentui/react"; import { IDropdownOption, IImageProps, Image, Stack, Text } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import StoredProcedure from "../../Tree/StoredProcedure"; import StoredProcedure from "../../Tree/StoredProcedure";
import { import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
import { InputParameter } from "./InputParameter"; import { InputParameter } from "./InputParameter";
interface ExecuteSprocParamsPaneProps { interface ExecuteSprocParamsPaneProps {
@@ -35,24 +33,11 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
const [partitionValue, setPartitionValue] = useState<string>(); // Defaulting to undefined here is important. It is not the same partition key as "" 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 [selectedKey, setSelectedKey] = React.useState<IDropdownOption>({ key: "string", text: "" });
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const onPartitionKeyChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => { const onPartitionKeyChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
setSelectedKey(item); setSelectedKey(item);
}; };
const genericPaneProps: GenericRightPaneProps = {
expandConsole,
formError: formError,
formErrorDetail: formErrorsDetails,
id: "executesprocparamspane",
isExecuting: isLoading,
title: "Input parameters",
submitButtonText: "Execute",
onClose: () => closePanel(),
onSubmit: () => submit(),
};
const validateUnwrappedParams = (): boolean => { const validateUnwrappedParams = (): boolean => {
const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValues; const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValues;
for (let i = 0; i < unwrappedParams.length; i++) { for (let i = 0; i < unwrappedParams.length; i++) {
@@ -66,7 +51,7 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
const setInvalidParamError = (invalidParam: string): void => { const setInvalidParamError = (invalidParam: string): void => {
setFormError(`Invalid param specified: ${invalidParam}`); setFormError(`Invalid param specified: ${invalidParam}`);
setFormErrorsDetails(`Invalid param specified: ${invalidParam} is not a valid literal value`); logConsoleError(`Invalid param specified: ${invalidParam} is not a valid literal value`);
}; };
const submit = (): void => { const submit = (): void => {
@@ -128,8 +113,16 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
setParamKeyValues(cloneParamKeyValue); setParamKeyValues(cloneParamKeyValue);
}; };
const props: RightPaneFormProps = {
expandConsole,
formError: formError,
isExecuting: isLoading,
submitButtonText: "Execute",
onSubmit: () => submit(),
};
return ( return (
<GenericRightPaneComponent {...genericPaneProps}> <RightPaneForm {...props}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">
<div className="panelMainContent"> <div className="panelMainContent">
<InputParameter <InputParameter
@@ -169,6 +162,6 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
</Stack> </Stack>
</div> </div>
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };
@@ -1,126 +0,0 @@
import { IconButton, PrimaryButton } from "@fluentui/react";
import React, { FunctionComponent, ReactNode } from "react";
import ErrorRedIcon from "../../../../images/error_red.svg";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import { KeyCodes } from "../../../Common/Constants";
export interface GenericRightPaneProps {
expandConsole: () => void;
formError: string;
formErrorDetail: string;
id: string;
isExecuting: boolean;
onClose: () => void;
onSubmit: () => void;
submitButtonText: string;
title: string;
isSubmitButtonHidden?: boolean;
children?: ReactNode;
}
export const GenericRightPaneComponent: FunctionComponent<GenericRightPaneProps> = ({
expandConsole,
formError,
formErrorDetail,
id,
isExecuting,
onClose,
onSubmit,
submitButtonText,
title,
isSubmitButtonHidden,
children,
}: GenericRightPaneProps) => {
const getPanelHeight = (): number => {
const notificationConsoleElement: HTMLElement = document.getElementById("explorerNotificationConsole");
return window.innerHeight - $(notificationConsoleElement).height();
};
const panelHeight: number = getPanelHeight();
const renderPanelHeader = (): JSX.Element => {
return (
<div className="firstdivbg headerline">
<span id="databaseTitle" role="heading" aria-level={2}>
{title}
</span>
<IconButton
ariaLabel="Close pane"
title="Close pane"
onClick={onClose}
tabIndex={0}
className="closePaneBtn"
iconProps={{ iconName: "Cancel" }}
/>
</div>
);
};
const renderErrorSection = (): JSX.Element => {
return (
<div className="warningErrorContainer" aria-live="assertive" hidden={!formError}>
<div className="warningErrorContent">
<span>
<img className="paneErrorIcon" src={ErrorRedIcon} alt="Error" />
</span>
<span className="warningErrorDetailsLinkContainer">
<span className="formErrors" title={formError}>
{formError}
</span>
<a className="errorLink" role="link" hidden={!formErrorDetail} onClick={expandConsole}>
More details
</a>
</span>
</div>
</div>
);
};
const renderPanelFooter = (): JSX.Element => {
return (
<div className="paneFooter">
<div className="leftpanel-okbut">
<PrimaryButton
style={{ visibility: isSubmitButtonHidden ? "hidden" : "visible" }}
ariaLabel="Submit"
title="Submit"
onClick={onSubmit}
tabIndex={0}
className="genericPaneSubmitBtn"
text={submitButtonText}
/>
</div>
</div>
);
};
const renderLoadingScreen = (): JSX.Element => {
return (
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!isExecuting}>
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
</div>
);
};
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
if (event.keyCode === KeyCodes.Escape) {
onClose();
event.stopPropagation();
}
};
return (
<div tabIndex={-1} onKeyDown={onKeyDown}>
<div className="contextual-pane-out" onClick={onClose}></div>
<div className="contextual-pane" id={id} style={{ height: panelHeight }} onKeyDown={onKeyDown}>
<div className="panelContentWrapper">
{renderPanelHeader()}
{renderErrorSection()}
{children}
{renderPanelFooter()}
</div>
{renderLoadingScreen()}
</div>
</div>
);
};
@@ -27,51 +27,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"arcadiaToken": [Function], "arcadiaToken": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
"closeDialog": undefined, "closeDialog": undefined,
"closeSidePanel": undefined, "closeSidePanel": undefined,
"collapsedResourceTreeWidth": 36, "collapsedResourceTreeWidth": 36,
@@ -876,6 +831,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"isResourceTokenCollectionNodeSelected": [Function], "isResourceTokenCollectionNodeSelected": [Function],
"isSchemaEnabled": [Function], "isSchemaEnabled": [Function],
"isServerlessEnabled": [Function], "isServerlessEnabled": [Function],
"isShellEnabled": [Function],
"isSparkEnabled": [Function], "isSparkEnabled": [Function],
"isSparkEnabledForAccount": [Function], "isSparkEnabledForAccount": [Function],
"isSynapseLinkUpdating": [Function], "isSynapseLinkUpdating": [Function],
@@ -1,5 +1,5 @@
import { useBoolean } from "@fluentui/react-hooks";
import { IImageProps, Image, ImageFit, Stack, TextField } from "@fluentui/react"; import { IImageProps, Image, ImageFit, Stack, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import folderIcon from "../../../../images/folder_16x16.svg"; import folderIcon from "../../../../images/folder_16x16.svg";
import { logError } from "../../../Common/Logger"; import { logError } from "../../../Common/Logger";
@@ -8,10 +8,7 @@ import { userContext } from "../../../UserContext";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import QueryTab from "../../Tabs/QueryTab"; import QueryTab from "../../Tabs/QueryTab";
import { import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
interface LoadQueryPaneProps { interface LoadQueryPaneProps {
explorer: Explorer; explorer: Explorer;
@@ -24,7 +21,6 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
}: LoadQueryPaneProps): JSX.Element => { }: LoadQueryPaneProps): JSX.Element => {
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false); const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const [selectedFileName, setSelectedFileName] = useState<string>(""); const [selectedFileName, setSelectedFileName] = useState<string>("");
const [selectedFiles, setSelectedFiles] = useState<FileList>(); const [selectedFiles, setSelectedFiles] = useState<FileList>();
@@ -35,19 +31,6 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
className: "fileIcon", className: "fileIcon",
}; };
const title = "Load Query";
const genericPaneProps: GenericRightPaneProps = {
expandConsole: () => explorer.expandConsole(),
formError: formError,
formErrorDetail: formErrorsDetails,
id: "loadQueryPane",
isExecuting: isLoading,
title,
submitButtonText: "Load",
onClose: () => closePanel(),
onSubmit: () => submit(),
};
const onFileSelected = (e: React.ChangeEvent<HTMLInputElement>): void => { const onFileSelected = (e: React.ChangeEvent<HTMLInputElement>): void => {
const { files } = e.target; const { files } = e.target;
setSelectedFiles(files); setSelectedFiles(files);
@@ -56,10 +39,8 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
const submit = async (): Promise<void> => { const submit = async (): Promise<void> => {
setFormError(""); setFormError("");
setFormErrorsDetails("");
if (!selectedFiles || selectedFiles.length === 0) { if (!selectedFiles || selectedFiles.length === 0) {
setFormError("No file specified"); setFormError("No file specified");
setFormErrorsDetails("No file specified. Please input a file.");
logConsoleError("Could not load query -- No file specified. Please input a file."); logConsoleError("Could not load query -- No file specified. Please input a file.");
return; return;
} }
@@ -75,7 +56,6 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
} catch (error) { } catch (error) {
setLoadingFalse(); setLoadingFalse();
setFormError("Failed to load query"); setFormError("Failed to load query");
setFormErrorsDetails(`Failed to load query: ${error}`);
logConsoleError(`Failed to load query from file ${file.name}: ${error}`); logConsoleError(`Failed to load query from file ${file.name}: ${error}`);
} }
}; };
@@ -100,14 +80,20 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
reader.onerror = (): void => { reader.onerror = (): void => {
setFormError("Failed to load query"); setFormError("Failed to load query");
setFormErrorsDetails(`Failed to load query`);
logConsoleError(`Failed to load query from file ${file.name}`); logConsoleError(`Failed to load query from file ${file.name}`);
}; };
return reader.readAsText(file); return reader.readAsText(file);
}; };
const props: RightPaneFormProps = {
formError: formError,
isExecuting: isLoading,
submitButtonText: "Load",
onSubmit: () => submit(),
expandConsole: () => explorer.expandConsole(),
};
return ( return (
<GenericRightPaneComponent {...genericPaneProps}> <RightPaneForm {...props}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">
<div className="panelMainContent"> <div className="panelMainContent">
<Stack horizontal> <Stack horizontal>
@@ -132,6 +118,6 @@ export const LoadQueryPane: FunctionComponent<LoadQueryPaneProps> = ({
</Stack> </Stack>
</div> </div>
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };
@@ -1,16 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Load Query Pane should render Default properly 1`] = ` exports[`Load Query Pane should render Default properly 1`] = `
<GenericRightPaneComponent <RightPaneForm
expandConsole={[Function]} expandConsole={[Function]}
formError="" formError=""
formErrorDetail=""
id="loadQueryPane"
isExecuting={false} isExecuting={false}
onClose={[Function]}
onSubmit={[Function]} onSubmit={[Function]}
submitButtonText="Load" submitButtonText="Load"
title="Load Query"
> >
<div <div
className="panelFormWrapper" className="panelFormWrapper"
@@ -58,5 +54,5 @@ exports[`Load Query Pane should render Default properly 1`] = `
</Stack> </Stack>
</div> </div>
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
`; `;
@@ -1,4 +1,4 @@
import { shallow, ShallowWrapper } from "enzyme"; import { mount, shallow, ShallowWrapper } from "enzyme";
import React from "react"; import React from "react";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
@@ -36,7 +36,7 @@ describe("New Vertex Panel", () => {
it("should call form submit method", () => { it("should call form submit method", () => {
const onSubmitSpy = jest.fn(); const onSubmitSpy = jest.fn();
const newWrapper = shallow( const newWrapper = mount(
<NewVertexPanel <NewVertexPanel
explorer={fakeExplorer} explorer={fakeExplorer}
partitionKeyPropertyProp={undefined} partitionKeyPropertyProp={undefined}
@@ -61,7 +61,7 @@ describe("New Vertex Panel", () => {
const result = onSubmitSpy(fakeNewVertexData, onErrorSpy, onSuccessSpy); const result = onSubmitSpy(fakeNewVertexData, onErrorSpy, onSuccessSpy);
const newWrapper = shallow( const newWrapper = mount(
<NewVertexPanel <NewVertexPanel
explorer={fakeExplorer} explorer={fakeExplorer}
partitionKeyPropertyProp={undefined} partitionKeyPropertyProp={undefined}
@@ -3,9 +3,7 @@ import React, { FunctionComponent, useState } from "react";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NewVertexComponent } from "../../Graph/NewVertexComponent/NewVertexComponent"; import { NewVertexComponent } from "../../Graph/NewVertexComponent/NewVertexComponent";
import { PanelFooterComponent } from "../PanelFooterComponent"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen";
export interface INewVertexPanelProps { export interface INewVertexPanelProps {
explorer: Explorer; explorer: Explorer;
partitionKeyPropertyProp: string; partitionKeyPropertyProp: string;
@@ -21,14 +19,10 @@ export const NewVertexPanel: FunctionComponent<INewVertexPanelProps> = ({
}: INewVertexPanelProps): JSX.Element => { }: INewVertexPanelProps): JSX.Element => {
let newVertexDataValue: ViewModels.NewVertexData; let newVertexDataValue: ViewModels.NewVertexData;
const [errorMessage, setErrorMessage] = useState<string>(""); const [errorMessage, setErrorMessage] = useState<string>("");
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false); const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const buttonLabel = "OK";
const submit = (event: React.MouseEvent<HTMLFormElement>) => { const submit = () => {
event.preventDefault();
setErrorMessage(undefined); setErrorMessage(undefined);
setShowErrorDetails(false);
if (onSubmit !== undefined) { if (onSubmit !== undefined) {
setLoadingTrue(); setLoadingTrue();
onSubmit(newVertexDataValue, onError, onSuccess); onSubmit(newVertexDataValue, onError, onSuccess);
@@ -37,7 +31,6 @@ export const NewVertexPanel: FunctionComponent<INewVertexPanelProps> = ({
const onError = (errorMsg: string) => { const onError = (errorMsg: string) => {
setErrorMessage(errorMsg); setErrorMessage(errorMsg);
setShowErrorDetails(true);
setLoadingFalse(); setLoadingFalse();
}; };
@@ -49,17 +42,16 @@ export const NewVertexPanel: FunctionComponent<INewVertexPanelProps> = ({
const onChange = (newVertexData: ViewModels.NewVertexData) => { const onChange = (newVertexData: ViewModels.NewVertexData) => {
newVertexDataValue = newVertexData; newVertexDataValue = newVertexData;
}; };
const props: RightPaneFormProps = {
formError: errorMessage,
isExecuting: isLoading,
submitButtonText: "OK",
onSubmit: () => submit(),
expandConsole: openNotificationConsole,
};
return ( return (
<form className="panelFormWrapper" onSubmit={(event: React.MouseEvent<HTMLFormElement>) => submit(event)}> <RightPaneForm {...props}>
{errorMessage && (
<PanelInfoErrorComponent
message={errorMessage}
messageType="error"
showErrorDetails={showErrorDetails}
openNotificationConsole={openNotificationConsole}
/>
)}
<div className="panelMainContent"> <div className="panelMainContent">
<NewVertexComponent <NewVertexComponent
newVertexDataProp={newVertexDataValue} newVertexDataProp={newVertexDataValue}
@@ -67,8 +59,6 @@ export const NewVertexPanel: FunctionComponent<INewVertexPanelProps> = ({
onChangeProp={onChange} onChangeProp={onChange}
/> />
</div> </div>
<PanelFooterComponent buttonLabel={buttonLabel} /> </RightPaneForm>
{isLoading && <PanelLoadingScreen />}
</form>
); );
}; };
@@ -1,9 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`New Vertex Panel should render default property 1`] = ` exports[`New Vertex Panel should render default property 1`] = `
<form <RightPaneForm
className="panelFormWrapper" expandConsole={[Function]}
formError=""
isExecuting={false}
onSubmit={[Function]} onSubmit={[Function]}
submitButtonText="OK"
> >
<div <div
className="panelMainContent" className="panelMainContent"
@@ -13,8 +16,5 @@ exports[`New Vertex Panel should render default property 1`] = `
partitionKeyPropertyProp="" partitionKeyPropertyProp=""
/> />
</div> </div>
<PanelFooterComponent </RightPaneForm>
buttonLabel="OK"
/>
</form>
`; `;
-15
View File
@@ -1,15 +0,0 @@
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
export class PaneComponent {
constructor(data: any) {
return data.data;
}
}
export class CassandraAddCollectionPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: CassandraAddCollectionPaneTemplate,
};
}
}
@@ -8,6 +8,7 @@ export interface PanelInfoErrorProps {
link?: string; link?: string;
linkText?: string; linkText?: string;
openNotificationConsole?: () => void; openNotificationConsole?: () => void;
formError?: boolean;
} }
export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProps> = ({ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProps> = ({
@@ -7,15 +7,12 @@ import { JunoClient } from "../../../Juno/JunoClient";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { CodeOfConductComponent } from "../../Controls/NotebookGallery/CodeOfConductComponent"; import { CodeOfConduct } from "../../Controls/NotebookGallery/CodeOfConduct/CodeOfConduct";
import { GalleryTab } from "../../Controls/NotebookGallery/GalleryViewerComponent"; import { GalleryTab } from "../../Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil"; import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { SnapshotRequest } from "../../Notebook/NotebookComponent/types"; import { SnapshotRequest } from "../../Notebook/NotebookComponent/types";
import { import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent"; import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
export interface PublishNotebookPaneAProps { export interface PublishNotebookPaneAProps {
@@ -155,7 +152,6 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
clearPublishingMessage(); clearPublishingMessage();
setIsExecuting(false); setIsExecuting(false);
} }
closePanel(); closePanel();
}; };
@@ -170,15 +166,11 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
setFormErrorDetail(""); setFormErrorDetail("");
}; };
const props: GenericRightPaneProps = { const props: RightPaneFormProps = {
formError: formError, formError: formError,
formErrorDetail: formErrorDetail,
id: "publishnotebookpane",
isExecuting: isExecuting, isExecuting: isExecuting,
title: "Publish to gallery",
submitButtonText: "Publish", submitButtonText: "Publish",
onSubmit: () => submit(), onSubmit: () => submit(),
onClose: closePanel,
expandConsole: () => container.expandConsole(), expandConsole: () => container.expandConsole(),
isSubmitButtonHidden: !isCodeOfConductAccepted, isSubmitButtonHidden: !isCodeOfConductAccepted,
}; };
@@ -201,10 +193,10 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
onTakeSnapshot, onTakeSnapshot,
}; };
return ( return (
<GenericRightPaneComponent {...props}> <RightPaneForm {...props}>
{!isCodeOfConductAccepted ? ( {!isCodeOfConductAccepted ? (
<div style={{ padding: "25px", marginTop: "10px" }}> <div style={{ padding: "25px", marginTop: "10px" }}>
<CodeOfConductComponent <CodeOfConduct
junoClient={junoClient} junoClient={junoClient}
onAcceptCodeOfConduct={(isAccepted) => { onAcceptCodeOfConduct={(isAccepted) => {
setIsCodeOfConductAccepted(isAccepted); setIsCodeOfConductAccepted(isAccepted);
@@ -214,6 +206,6 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
) : ( ) : (
<PublishNotebookPaneComponent {...publishNotebookPaneProps} /> <PublishNotebookPaneComponent {...publishNotebookPaneProps} />
)} )}
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };
@@ -1,5 +1,5 @@
import { useBoolean } from "@fluentui/react-hooks";
import { Text, TextField } from "@fluentui/react"; import { Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import { Areas, SavedQueries } from "../../../Common/Constants"; import { Areas, SavedQueries } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
@@ -9,10 +9,7 @@ import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetr
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import QueryTab from "../../Tabs/QueryTab"; import QueryTab from "../../Tabs/QueryTab";
import { import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
interface SaveQueryPaneProps { interface SaveQueryPaneProps {
explorer: Explorer; explorer: Explorer;
@@ -25,32 +22,16 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
}: SaveQueryPaneProps): JSX.Element => { }: SaveQueryPaneProps): JSX.Element => {
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false); const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const [queryName, setQueryName] = useState<string>(""); const [queryName, setQueryName] = useState<string>("");
const setupSaveQueriesText = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`; const setupSaveQueriesText = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`;
const title = "Save Query"; const title = "Save Query";
const { canSaveQueries } = explorer; const { canSaveQueries } = explorer;
const genericPaneProps: GenericRightPaneProps = {
expandConsole: () => explorer.expandConsole(),
formError: formError,
formErrorDetail: formErrorsDetails,
id: "saveQueryPane",
isExecuting: isLoading,
title,
submitButtonText: canSaveQueries() ? "Save" : "Complete setup",
onClose: () => closePanel(),
onSubmit: () => {
canSaveQueries() ? submit() : setupQueries();
},
};
const submit = async (): Promise<void> => { const submit = async (): Promise<void> => {
setFormError(""); setFormError("");
setFormErrorsDetails("");
if (!canSaveQueries()) { if (!canSaveQueries()) {
setFormError("Cannot save query"); setFormError("Cannot save query");
setFormErrorsDetails("Failed to save query: account not set up to save queries");
logConsoleError("Failed to save query: account not setup to save queries"); logConsoleError("Failed to save query: account not setup to save queries");
} }
@@ -58,12 +39,10 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
const query: string = queryTab && queryTab.sqlQueryEditorContent(); const query: string = queryTab && queryTab.sqlQueryEditorContent();
if (!queryName || queryName.length === 0) { if (!queryName || queryName.length === 0) {
setFormError("No query name specified"); setFormError("No query name specified");
setFormErrorsDetails("No query name specified. Please specify a query name.");
logConsoleError("Could not save query -- No query name specified. Please specify a query name."); logConsoleError("Could not save query -- No query name specified. Please specify a query name.");
return; return;
} else if (!query || query.length === 0) { } else if (!query || query.length === 0) {
setFormError("Invalid query content specified"); setFormError("Invalid query content specified");
setFormErrorsDetails("Invalid query content specified. Please enter query content.");
logConsoleError("Could not save query -- Invalid query content specified. Please enter query content."); logConsoleError("Could not save query -- Invalid query content specified. Please enter query content.");
return; return;
} }
@@ -97,7 +76,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
setLoadingFalse(); setLoadingFalse();
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
setFormError("Failed to save query"); setFormError("Failed to save query");
setFormErrorsDetails(`Failed to save query: ${errorMessage}`); logConsoleError(`Failed to save query: ${errorMessage}`);
traceFailure( traceFailure(
Action.SaveQuery, Action.SaveQuery,
{ {
@@ -142,14 +121,23 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
startKey startKey
); );
setFormError("Failed to setup a container for saved queries"); setFormError("Failed to setup a container for saved queries");
setFormErrorsDetails(`Failed to setup a container for saved queries: ${errorMessage}`); logConsoleError(`Failed to setup a container for saved queries: ${errorMessage}`);
} finally { } finally {
setLoadingFalse(); setLoadingFalse();
} }
}; };
const props: RightPaneFormProps = {
expandConsole: () => explorer.expandConsole(),
formError: formError,
isExecuting: isLoading,
submitButtonText: canSaveQueries() ? "Save" : "Complete setup",
onSubmit: () => {
canSaveQueries() ? submit() : setupQueries();
},
};
return ( return (
<GenericRightPaneComponent {...genericPaneProps}> <RightPaneForm {...props}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">
<div className="panelMainContent"> <div className="panelMainContent">
{!canSaveQueries() ? ( {!canSaveQueries() ? (
@@ -158,6 +146,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
<TextField <TextField
id="saveQueryInput" id="saveQueryInput"
label="Name" label="Name"
autoFocus
styles={{ fieldGroup: { width: 300 } }} styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
setQueryName(newInput); setQueryName(newInput);
@@ -166,6 +155,6 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
)} )}
</div> </div>
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };
@@ -1,16 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Save Query Pane should render Default properly 1`] = ` exports[`Save Query Pane should render Default properly 1`] = `
<GenericRightPaneComponent <RightPaneForm
expandConsole={[Function]} expandConsole={[Function]}
formError="" formError=""
formErrorDetail=""
id="saveQueryPane"
isExecuting={false} isExecuting={false}
onClose={[Function]}
onSubmit={[Function]} onSubmit={[Function]}
submitButtonText="Complete setup" submitButtonText="Complete setup"
title="Save Query"
> >
<div <div
className="panelFormWrapper" className="panelFormWrapper"
@@ -25,5 +21,5 @@ exports[`Save Query Pane should render Default properly 1`] = `
</Text> </Text>
</div> </div>
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
`; `;
@@ -6,10 +6,7 @@ import Explorer from "../../Explorer";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil"; import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import NotebookV2Tab from "../../Tabs/NotebookV2Tab"; import NotebookV2Tab from "../../Tabs/NotebookV2Tab";
import { import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
export interface StringInputPanelProps { export interface StringInputPanelProps {
explorer: Explorer; explorer: Explorer;
@@ -40,7 +37,6 @@ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
}: StringInputPanelProps): JSX.Element => { }: StringInputPanelProps): JSX.Element => {
const [stringInput, setStringInput] = useState<string>(defaultInput); const [stringInput, setStringInput] = useState<string>(defaultInput);
const [formErrors, setFormErrors] = useState<string>(""); const [formErrors, setFormErrors] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false); const [isExecuting, setIsExecuting] = useState<boolean>(false);
const submit = async (): Promise<void> => { const submit = async (): Promise<void> => {
@@ -51,7 +47,6 @@ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
return; return;
} else { } else {
setFormErrors(""); setFormErrors("");
setFormErrorsDetails("");
} }
const clearMessage = logConsoleProgress(`${inProgressMessage} ${stringInput}`); const clearMessage = logConsoleProgress(`${inProgressMessage} ${stringInput}`);
@@ -78,32 +73,26 @@ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
error = JSON.stringify(reason); error = JSON.stringify(reason);
} }
// If it's an AjaxError (AjaxObservable), add more error
if (reason?.response?.message) { if (reason?.response?.message) {
error += `. ${reason.response.message}`; error += `. ${reason.response.message}`;
} }
setFormErrors(errorMessage); setFormErrors(errorMessage);
setFormErrorsDetails(`${errorMessage}: ${error}`);
logConsoleError(`${errorMessage} ${stringInput}: ${error}`); logConsoleError(`${errorMessage} ${stringInput}: ${error}`);
} finally { } finally {
setIsExecuting(false); setIsExecuting(false);
clearMessage(); clearMessage();
} }
}; };
const genericPaneProps: GenericRightPaneProps = { const props: RightPaneFormProps = {
formError: formErrors, formError: formErrors,
formErrorDetail: formErrorsDetails,
id: "stringInputPane",
isExecuting: isExecuting, isExecuting: isExecuting,
title: paneTitle,
submitButtonText: submitButtonLabel, submitButtonText: submitButtonLabel,
onClose: closePanel,
onSubmit: submit, onSubmit: submit,
expandConsole: () => container.expandConsole(), expandConsole: () => container.expandConsole(),
}; };
return ( return (
<GenericRightPaneComponent {...genericPaneProps}> <RightPaneForm {...props}>
<div className="paneMainContent"> <div className="paneMainContent">
<TextField <TextField
label={inputLabel} label={inputLabel}
@@ -117,6 +106,6 @@ export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
aria-label={inputLabel} aria-label={inputLabel}
/> />
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };
File diff suppressed because it is too large Load Diff
@@ -3,7 +3,7 @@ import * as ko from "knockout";
import React from "react"; import React from "react";
import Explorer from "../../../Explorer"; import Explorer from "../../../Explorer";
import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel"; import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel";
import { TableQuerySelectPanel } from "./index"; import { TableQuerySelectPanel } from "./TableQuerySelectPanel";
describe("Table query select Panel", () => { describe("Table query select Panel", () => {
const fakeExplorer = {} as Explorer; const fakeExplorer = {} as Explorer;
@@ -4,10 +4,7 @@ import { userContext } from "../../../../UserContext";
import Explorer from "../../../Explorer"; import Explorer from "../../../Explorer";
import * as Constants from "../../../Tables/Constants"; import * as Constants from "../../../Tables/Constants";
import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel"; import QueryViewModel from "../../../Tables/QueryBuilder/QueryViewModel";
import { import { RightPaneForm, RightPaneFormProps } from "../../RightPaneForm/RightPaneForm";
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../../GenericRightPaneComponent/GenericRightPaneComponent";
interface TableQuerySelectPanelProps { interface TableQuerySelectPanelProps {
explorer: Explorer; explorer: Explorer;
@@ -31,24 +28,20 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
]); ]);
const [isAvailableColumnChecked, setIsAvailableColumnChecked] = useState<boolean>(true); const [isAvailableColumnChecked, setIsAvailableColumnChecked] = useState<boolean>(true);
const genericPaneProps: GenericRightPaneProps = { const onSubmit = (): void => {
formError: "",
formErrorDetail: "",
id: "querySelectPane",
isExecuting: false,
title: "Select Column",
submitButtonText: "OK",
onClose: () => closePanel(),
onSubmit: () => submit(),
expandConsole: () => explorer.expandConsole(),
};
const submit = (): void => {
queryViewModel.selectText(getParameters()); queryViewModel.selectText(getParameters());
queryViewModel.getSelectMessage(); queryViewModel.getSelectMessage();
closePanel(); closePanel();
}; };
const props: RightPaneFormProps = {
formError: "",
isExecuting: false,
submitButtonText: "OK",
onSubmit,
expandConsole: () => explorer.expandConsole(),
};
const handleClick = (isChecked: boolean, selectedColumn: string): void => { const handleClick = (isChecked: boolean, selectedColumn: string): void => {
const columns = columnOptions.map((column) => { const columns = columnOptions.map((column) => {
if (column.columnName === selectedColumn) { if (column.columnName === selectedColumn) {
@@ -128,7 +121,7 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
}; };
return ( return (
<GenericRightPaneComponent {...genericPaneProps}> <RightPaneForm {...props}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">
<div className="panelMainContent"> <div className="panelMainContent">
<Text>Select the columns that you want to query.</Text> <Text>Select the columns that you want to query.</Text>
@@ -153,6 +146,6 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
</div> </div>
</div> </div>
</div> </div>
</GenericRightPaneComponent> </RightPaneForm>
); );
}; };
@@ -78,7 +78,7 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
return uploadFile(file.name, fileContent); return uploadFile(file.name, fileContent);
}; };
const genericPaneProps: RightPaneFormProps = { const props: RightPaneFormProps = {
expandConsole, expandConsole,
formError: formErrors, formError: formErrors,
isExecuting: isExecuting, isExecuting: isExecuting,
@@ -87,7 +87,7 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
}; };
return ( return (
<RightPaneForm {...genericPaneProps}> <RightPaneForm {...props}>
<div className="paneMainContent"> <div className="paneMainContent">
<Upload label="Select file to upload" accept={extensions} onUpload={updateSelectedFiles} /> <Upload label="Select file to upload" accept={extensions} onUpload={updateSelectedFiles} />
</div> </div>
@@ -50,7 +50,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explo
setFiles(event.target.files); setFiles(event.target.files);
}; };
const genericPaneProps: RightPaneFormProps = { const props: RightPaneFormProps = {
expandConsole: () => explorer.expandConsole(), expandConsole: () => explorer.expandConsole(),
formError, formError,
isExecuting: isExecuting, isExecuting: isExecuting,
@@ -89,7 +89,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explo
}; };
return ( return (
<RightPaneForm {...genericPaneProps}> <RightPaneForm {...props}>
<div className="paneMainContent"> <div className="paneMainContent">
<Upload <Upload
label="Select JSON Files" label="Select JSON Files"
File diff suppressed because it is too large Load Diff
+11 -10
View File
@@ -5,20 +5,20 @@ import { render } from "react-dom";
import ChevronRight from "../images/chevron-right.svg"; import ChevronRight from "../images/chevron-right.svg";
import "../less/hostedexplorer.less"; import "../less/hostedexplorer.less";
import { AuthType } from "./AuthType"; import { AuthType } from "./AuthType";
import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer";
import { DatabaseAccount } from "./Contracts/DataModels"; import { DatabaseAccount } from "./Contracts/DataModels";
import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel";
import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher";
import "./Explorer/Menus/NavBar/MeControlComponent.less"; import "./Explorer/Menus/NavBar/MeControlComponent.less";
import { useTokenMetadata } from "./hooks/usePortalAccessToken";
import { MeControl } from "./Platform/Hosted/Components/MeControl";
import "./Platform/Hosted/ConnectScreen.less";
import "./Shared/appInsights";
import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
import { useAADAuth } from "./hooks/useAADAuth"; import { useAADAuth } from "./hooks/useAADAuth";
import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton"; import { useTokenMetadata } from "./hooks/usePortalAccessToken";
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame"; import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
import { AccountSwitcher } from "./Platform/Hosted/Components/AccountSwitcher";
import { ConnectExplorer } from "./Platform/Hosted/Components/ConnectExplorer";
import { DirectoryPickerPanel } from "./Platform/Hosted/Components/DirectoryPickerPanel";
import { FeedbackCommandButton } from "./Platform/Hosted/Components/FeedbackCommandButton";
import { MeControl } from "./Platform/Hosted/Components/MeControl";
import { SignInButton } from "./Platform/Hosted/Components/SignInButton";
import "./Platform/Hosted/ConnectScreen.less";
import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils"; import { extractMasterKeyfromConnectionString } from "./Platform/Hosted/HostedUtils";
import "./Shared/appInsights";
initializeIcons(); initializeIcons();
@@ -31,7 +31,7 @@ const App: React.FunctionComponent = () => {
// For showing/hiding panel // For showing/hiding panel
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant } = useAADAuth(); const { isLoggedIn, armToken, graphToken, aadToken, account, tenantId, logout, login, switchTenant } = useAADAuth();
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>(); const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined); const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
const [connectionString, setConnectionString] = React.useState<string>(); const [connectionString, setConnectionString] = React.useState<string>();
@@ -50,6 +50,7 @@ const App: React.FunctionComponent = () => {
authType: AuthType.AAD, authType: AuthType.AAD,
databaseAccount, databaseAccount,
authorizationToken: armToken, authorizationToken: armToken,
aadToken,
}; };
} else if (authType === AuthType.EncryptedToken) { } else if (authType === AuthType.EncryptedToken) {
frameWindow.hostedConfig = { frameWindow.hostedConfig = {
+1
View File
@@ -7,6 +7,7 @@ export interface HostedExplorerChildFrame extends Window {
} }
export interface AAD { export interface AAD {
aadToken: string;
authType: AuthType.AAD; authType: AuthType.AAD;
databaseAccount: DatabaseAccount; databaseAccount: DatabaseAccount;
authorizationToken: string; authorizationToken: string;
+6 -3
View File
@@ -37,16 +37,19 @@
"CannotSave": "Cannot save the changes to the Dedicated gateway resource at the moment.", "CannotSave": "Cannot save the changes to the Dedicated gateway resource at the moment.",
"DedicatedGatewayEndpoint": "Dedicated gatewayEndpoint", "DedicatedGatewayEndpoint": "Dedicated gatewayEndpoint",
"NoValue": "", "NoValue": "",
"SKUDetails": "SKU Details:",
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory", "CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory", "CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory", "CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
"CosmosD32Details": "General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory",
"Cost": "Cost", "Cost": "Cost",
"CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.", "CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.",
"ConnectionString": "Connection String", "ConnectionString": "Connection String",
"ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ", "ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ",
"KeysBlade": "the keys blade", "KeysBlade": "the keys blade.",
"MetricsString": "Metrics",
"MetricsText": "Monitor the \"DedicatedGatewayMaximumCpuUsage\" and \"DedicatedGatewayAverageMemoryUsage\" in ",
"MetricsBlade": "the metrics blade.",
"ResizingDecisionText": "To understand if the dedicated gateway is the right size, ",
"ResizingDecisionLink": "learn more about dedicated gateway sizing.",
"WarningBannerOnUpdate": "Adding or modifying dedicated gateway instances may affect your bill.", "WarningBannerOnUpdate": "Adding or modifying dedicated gateway instances may affect your bill.",
"WarningBannerOnDelete": "After deprovisioning the dedicated gateway, you must update any applications using the old dedicated gateway connection string." "WarningBannerOnDelete": "After deprovisioning the dedicated gateway, you must update any applications using the old dedicated gateway connection string."
} }
-1
View File
@@ -226,7 +226,6 @@ const App: React.FunctionComponent = () => {
closePanel={closeSidePanel} closePanel={closeSidePanel}
isConsoleExpanded={isNotificationConsoleExpanded} isConsoleExpanded={isNotificationConsoleExpanded}
/> />
<div data-bind='component: { name: "cassandra-add-collection-pane", params: { data: cassandraAddCollectionPane} }' />
{showDialog && <Dialog {...dialogProps} />} {showDialog && <Dialog {...dialogProps} />}
</div> </div>
); );
+2
View File
@@ -13,6 +13,7 @@ export type Features = {
readonly enableSpark: boolean; readonly enableSpark: boolean;
readonly enableTtl: boolean; readonly enableTtl: boolean;
readonly executeSproc: boolean; readonly executeSproc: boolean;
readonly enableAadDataPlane: boolean;
readonly hostedDataExplorer: boolean; readonly hostedDataExplorer: boolean;
readonly junoEndpoint?: string; readonly junoEndpoint?: string;
readonly livyEndpoint?: string; readonly livyEndpoint?: string;
@@ -43,6 +44,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
return { return {
canExceedMaximumValue: "true" === get("canexceedmaximumvalue"), canExceedMaximumValue: "true" === get("canexceedmaximumvalue"),
cosmosdb: "true" === get("cosmosdb"), cosmosdb: "true" === get("cosmosdb"),
enableAadDataPlane: "true" === get("enableaaddataplane"),
enableChangeFeedPolicy: "true" === get("enablechangefeedpolicy"), enableChangeFeedPolicy: "true" === get("enablechangefeedpolicy"),
enableFixedCollectionWithSharedThroughput: "true" === get("enablefixedcollectionwithsharedthroughput"), enableFixedCollectionWithSharedThroughput: "true" === get("enablefixedcollectionwithsharedthroughput"),
enableKOPanel: "true" === get("enablekopanel"), enableKOPanel: "true" === get("enablekopanel"),
+5 -3
View File
@@ -190,7 +190,8 @@ describe("SelfServeUtils", () => {
max: 5, max: 5,
step: 1, step: 1,
uiType: "Spinner", uiType: "Spinner",
errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidThroughput'.", errorMessage:
"labelTkey, trueLabelTKey and falseLabelTKey are required for boolean input 'invalidThroughput'.",
}, },
children: [] as Node[], children: [] as Node[],
}, },
@@ -225,7 +226,8 @@ describe("SelfServeUtils", () => {
type: "boolean", type: "boolean",
labelTKey: "Invalid Enable Logging", labelTKey: "Invalid Enable Logging",
placeholderTKey: "placeholder text", placeholderTKey: "placeholder text",
errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidEnableLogging'.", errorMessage:
"labelTkey, trueLabelTKey and falseLabelTKey are required for boolean input 'invalidEnableLogging'.",
}, },
children: [] as Node[], children: [] as Node[],
}, },
@@ -252,7 +254,7 @@ describe("SelfServeUtils", () => {
type: "object", type: "object",
labelTKey: "Invalid Regions", labelTKey: "Invalid Regions",
placeholderTKey: "placeholder text", placeholderTKey: "placeholder text",
errorMessage: "label and choices are required for Choice input 'invalidRegions'.", errorMessage: "labelTKey and choices are required for Choice input 'invalidRegions'.",
}, },
children: [] as Node[], children: [] as Node[],
}, },
+5 -5
View File
@@ -207,8 +207,8 @@ const addToDescriptor = (
const getInput = (value: DecoratorProperties): AnyDisplay => { const getInput = (value: DecoratorProperties): AnyDisplay => {
switch (value.type) { switch (value.type) {
case "number": case "number":
if (!value.labelTKey || !value.step || !value.uiType || !value.min || !value.max) { if (!value.labelTKey || !value.uiType || !value.step || !value.max || value.min === undefined) {
value.errorMessage = `label, step, min, max and uiType are required for number input '${value.id}'.`; value.errorMessage = `labelTkey, step, min, max and uiType are required for number input '${value.id}'.`;
} }
return value as NumberInput; return value as NumberInput;
case "string": case "string":
@@ -219,17 +219,17 @@ const getInput = (value: DecoratorProperties): AnyDisplay => {
return value as DescriptionDisplay; return value as DescriptionDisplay;
} }
if (!value.labelTKey) { if (!value.labelTKey) {
value.errorMessage = `label is required for string input '${value.id}'.`; value.errorMessage = `labelTKey is required for string input '${value.id}'.`;
} }
return value as StringInput; return value as StringInput;
case "boolean": case "boolean":
if (!value.labelTKey || !value.trueLabelTKey || !value.falseLabelTKey) { if (!value.labelTKey || !value.trueLabelTKey || !value.falseLabelTKey) {
value.errorMessage = `label, truelabel and falselabel are required for boolean input '${value.id}'.`; value.errorMessage = `labelTkey, trueLabelTKey and falseLabelTKey are required for boolean input '${value.id}'.`;
} }
return value as BooleanInput; return value as BooleanInput;
default: default:
if (!value.labelTKey || !value.choices) { if (!value.labelTKey || !value.choices) {
value.errorMessage = `label and choices are required for Choice input '${value.id}'.`; value.errorMessage = `labelTKey and choices are required for Choice input '${value.id}'.`;
} }
return value as ChoiceInput; return value as ChoiceInput;
} }
+56 -27
View File
@@ -1,10 +1,12 @@
import { RefreshResult } from "../SelfServeTypes"; import { configContext } from "../../ConfigContext";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { armRequestWithoutPolling } from "../../Utils/arm/request"; import { armRequestWithoutPolling } from "../../Utils/arm/request";
import { configContext } from "../../ConfigContext"; import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
import { RefreshResult } from "../SelfServeTypes";
import SqlX from "./SqlX";
import { SqlxServiceResource, UpdateDedicatedGatewayRequestParameters } from "./SqlxTypes"; import { SqlxServiceResource, UpdateDedicatedGatewayRequestParameters } from "./SqlxTypes";
const apiVersion = "2020-06-01-preview"; const apiVersion = "2021-04-01-preview";
export enum ResourceStatus { export enum ResourceStatus {
Running = "Running", Running = "Running",
@@ -21,7 +23,7 @@ export interface DedicatedGatewayResponse {
} }
export const getPath = (subscriptionId: string, resourceGroup: string, name: string): string => { export const getPath = (subscriptionId: string, resourceGroup: string, name: string): string => {
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/sqlx`; return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/SqlDedicatedGateway`;
}; };
export const updateDedicatedGatewayResource = async (sku: string, instances: number): Promise<string> => { export const updateDedicatedGatewayResource = async (sku: string, instances: number): Promise<string> => {
@@ -30,39 +32,66 @@ export const updateDedicatedGatewayResource = async (sku: string, instances: num
properties: { properties: {
instanceSize: sku, instanceSize: sku,
instanceCount: instances, instanceCount: instances,
serviceType: "Sqlx", serviceType: "SqlDedicatedGateway",
}, },
}; };
const armRequestResult = await armRequestWithoutPolling({ const telemetryData = { ...body, httpMethod: "PUT", selfServeClassName: SqlX.name };
host: configContext.ARM_ENDPOINT, const updateTimeStamp = selfServeTraceStart(telemetryData);
path, let armRequestResult;
method: "PUT", try {
apiVersion, armRequestResult = await armRequestWithoutPolling({
body, host: configContext.ARM_ENDPOINT,
}); path,
return armRequestResult.operationStatusUrl; method: "PUT",
apiVersion,
body,
});
selfServeTraceSuccess(telemetryData, updateTimeStamp);
} catch (e) {
const failureTelemetry = { ...body, e, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, updateTimeStamp);
}
return armRequestResult?.operationStatusUrl;
}; };
export const deleteDedicatedGatewayResource = async (): Promise<string> => { export const deleteDedicatedGatewayResource = async (): Promise<string> => {
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
const armRequestResult = await armRequestWithoutPolling({ const telemetryData = { httpMethod: "DELETE", selfServeClassName: SqlX.name };
host: configContext.ARM_ENDPOINT, const deleteTimeStamp = selfServeTraceStart(telemetryData);
path, let armRequestResult;
method: "DELETE", try {
apiVersion, armRequestResult = await armRequestWithoutPolling({
}); host: configContext.ARM_ENDPOINT,
return armRequestResult.operationStatusUrl; path,
method: "DELETE",
apiVersion,
});
selfServeTraceSuccess(telemetryData, deleteTimeStamp);
} catch (e) {
const failureTelemetry = { e, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, deleteTimeStamp);
}
return armRequestResult?.operationStatusUrl;
}; };
export const getDedicatedGatewayResource = async (): Promise<SqlxServiceResource> => { export const getDedicatedGatewayResource = async (): Promise<SqlxServiceResource> => {
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
const armRequestResult = await armRequestWithoutPolling<SqlxServiceResource>({ const telemetryData = { httpMethod: "GET", selfServeClassName: SqlX.name };
host: configContext.ARM_ENDPOINT, const getResourceTimeStamp = selfServeTraceStart(telemetryData);
path, let armRequestResult;
method: "GET", try {
apiVersion, armRequestResult = await armRequestWithoutPolling<SqlxServiceResource>({
}); host: configContext.ARM_ENDPOINT,
return armRequestResult.result; path,
method: "GET",
apiVersion,
});
selfServeTraceSuccess(telemetryData, getResourceTimeStamp);
} catch (e) {
const failureTelemetry = { e, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getResourceTimeStamp);
}
return armRequestResult?.result;
}; };
export const getCurrentProvisioningState = async (): Promise<DedicatedGatewayResponse> => { export const getCurrentProvisioningState = async (): Promise<DedicatedGatewayResponse> => {
+85 -50
View File
@@ -23,7 +23,7 @@ const costPerHourValue: Description = {
textTKey: "CostText", textTKey: "CostText",
type: DescriptionType.Text, type: DescriptionType.Text,
link: { link: {
href: "https://azure.microsoft.com/en-us/pricing/details/cosmos-db/", href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing",
textTKey: "DedicatedGatewayPricing", textTKey: "DedicatedGatewayPricing",
}, },
}; };
@@ -37,43 +37,56 @@ const connectionStringValue: Description = {
}, },
}; };
const metricsStringValue: Description = {
textTKey: "MetricsText",
type: DescriptionType.Text,
link: {
href: generateBladeLink(BladeType.Metrics),
textTKey: "MetricsBlade",
},
};
const resizingDecisionValue: Description = {
textTKey: "ResizingDecisionText",
type: DescriptionType.Text,
link: {
href: "https://aka.ms/cosmos-db-dedicated-gateway-size",
textTKey: "ResizingDecisionLink",
},
};
const CosmosD4s = "Cosmos.D4s"; const CosmosD4s = "Cosmos.D4s";
const CosmosD8s = "Cosmos.D8s"; const CosmosD8s = "Cosmos.D8s";
const CosmosD16s = "Cosmos.D16s"; const CosmosD16s = "Cosmos.D16s";
const CosmosD32s = "Cosmos.D32s";
const getSKUDetails = (sku: string): string => {
if (sku === CosmosD4s) {
return "CosmosD4Details";
} else if (sku === CosmosD8s) {
return "CosmosD8Details";
} else if (sku === CosmosD16s) {
return "CosmosD16Details";
} else if (sku === CosmosD32s) {
return "CosmosD32Details";
}
return "Not Supported Yet";
};
const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInput>): Map<string, SmartUiInput> => { const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
currentValues.set("sku", { value: newValue }); currentValues.set("sku", { value: newValue });
currentValues.set("skuDetails", {
value: { textTKey: getSKUDetails(`${newValue.toString()}`), type: DescriptionType.Text } as Description,
});
currentValues.set("costPerHour", { value: costPerHourValue }); currentValues.set("costPerHour", { value: costPerHourValue });
return currentValues; return currentValues;
}; };
const onNumberOfInstancesChange = ( const onNumberOfInstancesChange = (
newValue: InputType, newValue: InputType,
currentValues: Map<string, SmartUiInput> currentValues: Map<string, SmartUiInput>,
baselineValues: Map<string, SmartUiInput>
): Map<string, SmartUiInput> => { ): Map<string, SmartUiInput> => {
currentValues.set("instances", { value: newValue }); currentValues.set("instances", { value: newValue });
currentValues.set("warningBanner", { const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean;
value: { textTKey: "WarningBannerOnUpdate" } as Description, const baselineInstances = baselineValues.get("instances")?.value as number;
hidden: false, if (!dedicatedGatewayOriginallyEnabled || baselineInstances !== newValue) {
}); currentValues.set("warningBanner", {
value: {
textTKey: "WarningBannerOnUpdate",
link: {
href: "https://aka.ms/cosmos-db-dedicated-gateway-overview",
textTKey: "DedicatedGatewayPricing",
},
} as Description,
hidden: false,
});
} else {
currentValues.set("warningBanner", undefined);
}
return currentValues; return currentValues;
}; };
@@ -87,10 +100,11 @@ const onEnableDedicatedGatewayChange = (
if (dedicatedGatewayOriginallyEnabled === newValue) { if (dedicatedGatewayOriginallyEnabled === newValue) {
currentValues.set("sku", baselineValues.get("sku")); currentValues.set("sku", baselineValues.get("sku"));
currentValues.set("instances", baselineValues.get("instances")); currentValues.set("instances", baselineValues.get("instances"));
currentValues.set("skuDetails", baselineValues.get("skuDetails"));
currentValues.set("costPerHour", baselineValues.get("costPerHour")); currentValues.set("costPerHour", baselineValues.get("costPerHour"));
currentValues.set("warningBanner", baselineValues.get("warningBanner")); currentValues.set("warningBanner", baselineValues.get("warningBanner"));
currentValues.set("connectionString", baselineValues.get("connectionString")); currentValues.set("connectionString", baselineValues.get("connectionString"));
currentValues.set("metricsString", baselineValues.get("metricsString"));
currentValues.set("resizingDecisionString", baselineValues.get("resizingDecisionString"));
return currentValues; return currentValues;
} }
@@ -100,7 +114,7 @@ const onEnableDedicatedGatewayChange = (
value: { value: {
textTKey: "WarningBannerOnUpdate", textTKey: "WarningBannerOnUpdate",
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing",
textTKey: "DedicatedGatewayPricing", textTKey: "DedicatedGatewayPricing",
}, },
} as Description, } as Description,
@@ -111,7 +125,7 @@ const onEnableDedicatedGatewayChange = (
value: { value: {
textTKey: "WarningBannerOnDelete", textTKey: "WarningBannerOnDelete",
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://aka.ms/cosmos-db-dedicated-gateway-overview",
textTKey: "DeprovisioningDetailsText", textTKey: "DeprovisioningDetailsText",
}, },
} as Description, } as Description,
@@ -132,18 +146,22 @@ const onEnableDedicatedGatewayChange = (
disabled: dedicatedGatewayOriginallyEnabled, disabled: dedicatedGatewayOriginallyEnabled,
}); });
currentValues.set("skuDetails", {
value: { textTKey: getSKUDetails(`${currentValues.get("sku").value}`), type: DescriptionType.Text } as Description,
hidden: hideAttributes,
disabled: dedicatedGatewayOriginallyEnabled,
});
currentValues.set("costPerHour", { value: costPerHourValue, hidden: hideAttributes }); currentValues.set("costPerHour", { value: costPerHourValue, hidden: hideAttributes });
currentValues.set("connectionString", { currentValues.set("connectionString", {
value: connectionStringValue, value: connectionStringValue,
hidden: !newValue || !dedicatedGatewayOriginallyEnabled, hidden: !newValue || !dedicatedGatewayOriginallyEnabled,
}); });
currentValues.set("metricsString", {
value: metricsStringValue,
hidden: !newValue || !dedicatedGatewayOriginallyEnabled,
});
currentValues.set("resizingDecisionString", {
value: resizingDecisionValue,
hidden: !newValue || !dedicatedGatewayOriginallyEnabled,
});
return currentValues; return currentValues;
}; };
@@ -151,7 +169,6 @@ const skuDropDownItems: ChoiceItem[] = [
{ labelTKey: "CosmosD4s", key: CosmosD4s }, { labelTKey: "CosmosD4s", key: CosmosD4s },
{ labelTKey: "CosmosD8s", key: CosmosD8s }, { labelTKey: "CosmosD8s", key: CosmosD8s },
{ labelTKey: "CosmosD16s", key: CosmosD16s }, { labelTKey: "CosmosD16s", key: CosmosD16s },
{ labelTKey: "CosmosD32s", key: CosmosD32s },
]; ];
const getSkus = async (): Promise<ChoiceItem[]> => { const getSkus = async (): Promise<ChoiceItem[]> => {
@@ -184,7 +201,6 @@ export default class SqlX extends SelfServeBaseClass {
currentValues.set("warningBanner", undefined); currentValues.set("warningBanner", undefined);
//TODO : Add try catch for each RP call and return relevant notifications
if (dedicatedGatewayOriginallyEnabled) { if (dedicatedGatewayOriginallyEnabled) {
if (!dedicatedGatewayCurrentlyEnabled) { if (!dedicatedGatewayCurrentlyEnabled) {
const operationStatusUrl = await deleteDedicatedGatewayResource(); const operationStatusUrl = await deleteDedicatedGatewayResource();
@@ -206,9 +222,11 @@ export default class SqlX extends SelfServeBaseClass {
}, },
}; };
} else { } else {
// Check for scaling up/down/in/out const sku = currentValues.get("sku")?.value as string;
const instances = currentValues.get("instances").value as number;
const operationStatusUrl = await updateDedicatedGatewayResource(sku, instances);
return { return {
operationStatusUrl: undefined, operationStatusUrl: operationStatusUrl,
portalNotification: { portalNotification: {
initialize: { initialize: {
titleTKey: "UpdateInitializeTitle", titleTKey: "UpdateInitializeTitle",
@@ -255,24 +273,37 @@ export default class SqlX extends SelfServeBaseClass {
defaults.set("enableDedicatedGateway", { value: false }); defaults.set("enableDedicatedGateway", { value: false });
defaults.set("sku", { value: CosmosD4s, hidden: true }); defaults.set("sku", { value: CosmosD4s, hidden: true });
defaults.set("instances", { value: await getInstancesMin(), hidden: true }); defaults.set("instances", { value: await getInstancesMin(), hidden: true });
defaults.set("skuDetails", undefined);
defaults.set("costPerHour", undefined); defaults.set("costPerHour", undefined);
defaults.set("connectionString", undefined); defaults.set("connectionString", undefined);
defaults.set("metricsString", {
value: undefined,
hidden: true,
});
defaults.set("resizingDecisionString", {
value: undefined,
hidden: true,
});
const response = await getCurrentProvisioningState(); const response = await getCurrentProvisioningState();
if (response.status && response.status !== "Deleting") { if (response.status && response.status !== "Deleting") {
defaults.set("enableDedicatedGateway", { value: true }); defaults.set("enableDedicatedGateway", { value: true });
defaults.set("sku", { value: response.sku, disabled: true }); defaults.set("sku", { value: response.sku, disabled: true });
defaults.set("instances", { value: response.instances, disabled: true }); defaults.set("instances", { value: response.instances, disabled: false });
defaults.set("costPerHour", { value: costPerHourValue }); defaults.set("costPerHour", { value: costPerHourValue });
defaults.set("skuDetails", {
value: { textTKey: getSKUDetails(`${defaults.get("sku").value}`), type: DescriptionType.Text } as Description,
hidden: false,
});
defaults.set("connectionString", { defaults.set("connectionString", {
value: connectionStringValue, value: connectionStringValue,
hidden: false, hidden: false,
}); });
defaults.set("metricsString", {
value: metricsStringValue,
hidden: false,
});
defaults.set("resizingDecisionString", {
value: resizingDecisionValue,
hidden: false,
});
} }
defaults.set("warningBanner", undefined); defaults.set("warningBanner", undefined);
@@ -289,7 +320,7 @@ export default class SqlX extends SelfServeBaseClass {
textTKey: "DedicatedGatewayDescription", textTKey: "DedicatedGatewayDescription",
type: DescriptionType.Text, type: DescriptionType.Text,
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://aka.ms/cosmos-db-dedicated-gateway-overview",
textTKey: "LearnAboutDedicatedGateway", textTKey: "LearnAboutDedicatedGateway",
}, },
}, },
@@ -312,12 +343,6 @@ export default class SqlX extends SelfServeBaseClass {
}) })
sku: ChoiceItem; sku: ChoiceItem;
@Values({
labelTKey: "SKUDetails",
isDynamicDescription: true,
})
skuDetails: string;
@OnChange(onNumberOfInstancesChange) @OnChange(onNumberOfInstancesChange)
@Values({ @Values({
labelTKey: "NumberOfInstances", labelTKey: "NumberOfInstances",
@@ -328,6 +353,16 @@ export default class SqlX extends SelfServeBaseClass {
}) })
instances: number; instances: number;
@Values({
description: metricsStringValue,
})
metricsString: string;
@Values({
description: resizingDecisionValue,
})
resizingDecisionString: string;
@Values({ @Values({
labelTKey: "Cost", labelTKey: "Cost",
isDynamicDescription: true, isDynamicDescription: true,
+5 -5
View File
@@ -1,11 +1,11 @@
import "@jupyterlab/terminal/style/index.css";
import "./index.css";
import { ServerConnection } from "@jupyterlab/services"; import { ServerConnection } from "@jupyterlab/services";
import { JupyterLabAppFactory } from "./JupyterLabAppFactory"; import "@jupyterlab/terminal/style/index.css";
import { HttpHeaders, TerminalQueryParams } from "../Common/Constants";
import { Action } from "../Shared/Telemetry/TelemetryConstants"; import { Action } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../UserContext"; import { updateUserContext } from "../UserContext";
import { HttpHeaders, TerminalQueryParams } from "../Common/Constants"; import "./index.css";
import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
const getUrlVars = (): { [key: string]: string } => { const getUrlVars = (): { [key: string]: string } => {
const vars: { [key: string]: string } = {}; const vars: { [key: string]: string } = {};
@@ -50,7 +50,7 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect
const main = async (): Promise<void> => { const main = async (): Promise<void> => {
const urlVars = getUrlVars(); const urlVars = getUrlVars();
// Initialize userContext. Currently only subscrptionId is required by TelemetryProcessor // Initialize userContext. Currently only subscriptionId is required by TelemetryProcessor
updateUserContext({ updateUserContext({
subscriptionId: urlVars[TerminalQueryParams.SubscriptionId], subscriptionId: urlVars[TerminalQueryParams.SubscriptionId],
}); });
+1
View File
@@ -11,6 +11,7 @@ interface UserContext {
readonly resourceGroup?: string; readonly resourceGroup?: string;
readonly databaseAccount?: DatabaseAccount; readonly databaseAccount?: DatabaseAccount;
readonly endpoint?: string; readonly endpoint?: string;
readonly aadToken?: string;
readonly accessToken?: string; readonly accessToken?: string;
readonly authorizationToken?: string; readonly authorizationToken?: string;
readonly resourceToken?: string; readonly resourceToken?: string;
+8 -1
View File
@@ -25,6 +25,7 @@ interface ReturnType {
isLoggedIn: boolean; isLoggedIn: boolean;
graphToken: string; graphToken: string;
armToken: string; armToken: string;
aadToken: string;
login: () => void; login: () => void;
logout: () => void; logout: () => void;
tenantId: string; tenantId: string;
@@ -40,6 +41,7 @@ export function useAADAuth(): ReturnType {
const [tenantId, setTenantId] = React.useState<string>(cachedTenantId); const [tenantId, setTenantId] = React.useState<string>(cachedTenantId);
const [graphToken, setGraphToken] = React.useState<string>(); const [graphToken, setGraphToken] = React.useState<string>();
const [armToken, setArmToken] = React.useState<string>(); const [armToken, setArmToken] = React.useState<string>();
const [aadToken, setAadToken] = React.useState<string>();
msalInstance.setActiveAccount(account); msalInstance.setActiveAccount(account);
const login = React.useCallback(async () => { const login = React.useCallback(async () => {
@@ -79,9 +81,13 @@ export function useAADAuth(): ReturnType {
authority: `https://login.microsoftonline.com/${tenantId}`, authority: `https://login.microsoftonline.com/${tenantId}`,
scopes: ["https://management.azure.com//.default"], scopes: ["https://management.azure.com//.default"],
}), }),
]).then(([graphTokenResponse, armTokenResponse]) => { msalInstance.acquireTokenSilent({
scopes: ["https://cosmos.azure.com/.default"],
}),
]).then(([graphTokenResponse, armTokenResponse, aadTokenResponse]) => {
setGraphToken(graphTokenResponse.accessToken); setGraphToken(graphTokenResponse.accessToken);
setArmToken(armTokenResponse.accessToken); setArmToken(armTokenResponse.accessToken);
setAadToken(aadTokenResponse.accessToken);
}); });
} }
}, [account, tenantId]); }, [account, tenantId]);
@@ -92,6 +98,7 @@ export function useAADAuth(): ReturnType {
isLoggedIn, isLoggedIn,
graphToken, graphToken,
armToken, armToken,
aadToken,
login, login,
logout, logout,
switchTenant, switchTenant,
+1
View File
@@ -83,6 +83,7 @@ async function configureHostedWithAAD(config: AAD, explorerParams: ExplorerParam
updateUserContext({ updateUserContext({
authType: AuthType.AAD, authType: AuthType.AAD,
authorizationToken: `Bearer ${config.authorizationToken}`, authorizationToken: `Bearer ${config.authorizationToken}`,
aadToken: config.aadToken,
}); });
const account = config.databaseAccount; const account = config.databaseAccount;
const accountResourceId = account.id; const accountResourceId = account.id;
+1 -2
View File
@@ -8,8 +8,7 @@ i18n
.init({ .init({
fallbackLng: "en", fallbackLng: "en",
detection: { order: ["navigator", "cookie", "localStorage", "sessionStorage", "querystring", "htmlTag"] }, detection: { order: ["navigator", "cookie", "localStorage", "sessionStorage", "querystring", "htmlTag"] },
// temporarily setting debug to true to investigate loading issues in prod debug: process.env.NODE_ENV === "development",
debug: true,
keySeparator: ".", keySeparator: ".",
interpolation: { interpolation: {
formatSeparator: ",", formatSeparator: ",",
+6 -6
View File
@@ -15,16 +15,16 @@ test("Cassandra keyspace and table CRUD", async () => {
}); });
await explorer.click('[data-test="New Table"]'); await explorer.click('[data-test="New Table"]');
await explorer.click('[data-test="addCollection-keyspaceId"]'); await explorer.click('[aria-label="Keyspace id"]');
await explorer.fill('[data-test="addCollection-keyspaceId"]', keyspaceId); await explorer.fill('[aria-label="Keyspace id"]', keyspaceId);
await explorer.click('[data-test="addCollection-tableId"]'); await explorer.click('[aria-label="addCollection-tableId"]');
await explorer.fill('[data-test="addCollection-tableId"]', tableId); await explorer.fill('[aria-label="addCollection-tableId"]', tableId);
await explorer.click('[aria-label="Add Table"] [data-test="addCollection-createCollection"]'); await explorer.click("#sidePanelOkButton");
await safeClick(explorer, `.nodeItem >> text=${keyspaceId}`); await safeClick(explorer, `.nodeItem >> text=${keyspaceId}`);
await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`); await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")'); await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")');
await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId);
await explorer.click('[aria-label="Submit"]'); await explorer.click('[aria-label="OK"]');
await explorer.click(`[data-test="${keyspaceId}"] [aria-label="More"]`); await explorer.click(`[data-test="${keyspaceId}"] [aria-label="More"]`);
await explorer.click('button[role="menuitem"]:has-text("Delete Keyspace")'); await explorer.click('button[role="menuitem"]:has-text("Delete Keyspace")');
await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]');
+1 -1
View File
@@ -26,7 +26,7 @@ test("Graph CRUD", async () => {
await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Graph")'); await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Graph")');
await explorer.fill('text=* Confirm by typing the graph id >> input[type="text"]', containerId); await explorer.fill('text=* Confirm by typing the graph id >> input[type="text"]', containerId);
await explorer.click('[aria-label="Submit"]'); await explorer.click('[aria-label="OK"]');
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);
await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('button[role="menuitem"]:has-text("Delete Database")');
await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]');
+2 -2
View File
@@ -23,7 +23,7 @@ test("Mongo CRUD", async () => {
await safeClick(explorer, `.nodeItem >> text=${databaseId}`); await safeClick(explorer, `.nodeItem >> text=${databaseId}`);
await safeClick(explorer, `.nodeItem >> text=${containerId}`); await safeClick(explorer, `.nodeItem >> text=${containerId}`);
// Create indexing policy // Create indexing policy
await safeClick(explorer, ".nodeItem >> text=Setting"); await safeClick(explorer, ".nodeItem >> text=Settings");
await explorer.click('button[role="tab"]:has-text("Indexing Policy")'); await explorer.click('button[role="tab"]:has-text("Indexing Policy")');
await explorer.click('[aria-label="Index Field Name 0"]'); await explorer.click('[aria-label="Index Field Name 0"]');
await explorer.fill('[aria-label="Index Field Name 0"]', "foo"); await explorer.fill('[aria-label="Index Field Name 0"]', "foo");
@@ -37,7 +37,7 @@ test("Mongo CRUD", async () => {
await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Collection")'); await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Collection")');
await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId); await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId);
await explorer.click('[aria-label="Submit"]'); await explorer.click('[aria-label="OK"]');
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);
await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('button[role="menuitem"]:has-text("Delete Database")');
await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]');
+1 -1
View File
@@ -26,7 +26,7 @@ test("Mongo CRUD", async () => {
await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Collection")'); await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Collection")');
await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId); await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId);
await explorer.click('[aria-label="Submit"]'); await explorer.click('[aria-label="OK"]');
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);
await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('button[role="menuitem"]:has-text("Delete Database")');
await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]');
+1 -1
View File
@@ -23,7 +23,7 @@ test("SQL CRUD", async () => {
await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`); await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Container")'); await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Container")');
await explorer.fill('text=* Confirm by typing the container id >> input[type="text"]', containerId); await explorer.fill('text=* Confirm by typing the container id >> input[type="text"]', containerId);
await explorer.click('[aria-label="Submit"]'); await explorer.click('[aria-label="OK"]');
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`); await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);
await explorer.click('button[role="menuitem"]:has-text("Delete Database")'); await explorer.click('button[role="menuitem"]:has-text("Delete Database")');
await explorer.click('text=* Confirm by typing the database id >> input[type="text"]'); await explorer.click('text=* Confirm by typing the database id >> input[type="text"]');
+1 -1
View File
@@ -21,6 +21,6 @@ test("Tables CRUD", async () => {
await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`); await safeClick(explorer, `[data-test="${tableId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")'); await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Table")');
await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId); await explorer.fill('text=* Confirm by typing the table id >> input[type="text"]', tableId);
await explorer.click('[aria-label="Submit"]'); await explorer.click('[aria-label="OK"]');
await expect(explorer).not.toHaveText(".dataResourceTree", tableId); await expect(explorer).not.toHaveText(".dataResourceTree", tableId);
}); });
-1
View File
@@ -60,7 +60,6 @@
"./src/Explorer/Notebook/NotebookUtil.ts", "./src/Explorer/Notebook/NotebookUtil.ts",
"./src/Explorer/OpenFullScreen.test.tsx", "./src/Explorer/OpenFullScreen.test.tsx",
"./src/Explorer/OpenFullScreen.tsx", "./src/Explorer/OpenFullScreen.tsx",
"./src/Explorer/Panes/PaneComponents.ts",
"./src/Explorer/Panes/PanelFooterComponent.tsx", "./src/Explorer/Panes/PanelFooterComponent.tsx",
"./src/Explorer/Panes/PanelInfoErrorComponent.tsx", "./src/Explorer/Panes/PanelInfoErrorComponent.tsx",
"./src/Explorer/Panes/PanelLoadingScreen.tsx", "./src/Explorer/Panes/PanelLoadingScreen.tsx",