mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-06 11:11:23 +00:00
Compare commits
2 Commits
users/artr
...
fix_a11y_D
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceb5074db3 | ||
|
|
bb1bbc2f70 |
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@@ -12,8 +12,7 @@
|
||||
"--inspect-brk",
|
||||
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||
"--runInBand",
|
||||
"--coverage",
|
||||
"false"
|
||||
"--coverage", "false"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
@@ -27,8 +26,7 @@
|
||||
"--inspect-brk",
|
||||
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||
"${fileBasenameNoExtension}",
|
||||
"--coverage",
|
||||
"false",
|
||||
"--coverage", "false",
|
||||
// "--watch",
|
||||
// // --no-cache only used to make --watch work. Otherwise jest ignores the breakpoints.
|
||||
// // https://github.com/facebook/jest/issues/6683
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
|
||||
"isTerminalEnabled" : true,
|
||||
"isPhoenixEnabled" : true
|
||||
"isTerminalEnabled" : true
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||
"isTerminalEnabled" : false,
|
||||
"isPhoenixEnabled" : false
|
||||
"isTerminalEnabled" : false
|
||||
}
|
||||
|
||||
@@ -2885,6 +2885,10 @@ a:link {
|
||||
|
||||
.settingsSectionPart {
|
||||
padding-left: 8px;
|
||||
label {
|
||||
padding: 0 !important;
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.settingsSectionLabel {
|
||||
|
||||
3213
package-lock.json
generated
3213
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -236,12 +236,13 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
|
||||
it("returns a production endpoint", () => {
|
||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||
const endpoint = getEndpoint();
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||
});
|
||||
|
||||
it("returns a development endpoint", () => {
|
||||
const endpoint = getEndpoint("https://localhost:1234");
|
||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||
const endpoint = getEndpoint();
|
||||
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
|
||||
});
|
||||
|
||||
@@ -249,7 +250,7 @@ describe("MongoProxyClient", () => {
|
||||
updateUserContext({
|
||||
authType: AuthType.EncryptedToken,
|
||||
});
|
||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||
const endpoint = getEndpoint();
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||
import queryString from "querystring";
|
||||
import { allowedMongoProxyEndpoints, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
@@ -337,17 +336,14 @@ export function createMongoCollectionWithProxy(
|
||||
}
|
||||
|
||||
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||
const endpoint =
|
||||
hasFlag(userContext.features.mongoProxyAPIs, feature) &&
|
||||
validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
|
||||
? userContext.features.mongoProxyEndpoint
|
||||
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
|
||||
return getEndpoint(endpoint);
|
||||
return hasFlag(userContext.features.mongoProxyAPIs, feature)
|
||||
? getEndpoint(userContext.features.mongoProxyEndpoint)
|
||||
: getEndpoint();
|
||||
}
|
||||
|
||||
export function getEndpoint(endpoint: string): string {
|
||||
let url = endpoint + "/api/mongo/explorer";
|
||||
export function getEndpoint(customEndpoint?: string): string {
|
||||
let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
url += "/api/mongo/explorer";
|
||||
|
||||
if (userContext.authType === AuthType.EncryptedToken) {
|
||||
url = url.replace("api/mongo", "api/guest/mongo");
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
import {
|
||||
allowedAadEndpoints,
|
||||
allowedArcadiaEndpoints,
|
||||
allowedArmEndpoints,
|
||||
allowedBackendEndpoints,
|
||||
allowedEmulatorEndpoints,
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMongoBackendEndpoints,
|
||||
allowedMsalRedirectEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointValidation";
|
||||
import { JunoEndpoints } from "Common/Constants";
|
||||
|
||||
export enum Platform {
|
||||
Portal = "Portal",
|
||||
@@ -20,7 +8,7 @@ export enum Platform {
|
||||
|
||||
export interface ConfigContext {
|
||||
platform: Platform;
|
||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||
allowedParentFrameOrigins: string[];
|
||||
gitSha?: string;
|
||||
proxyPath?: string;
|
||||
AAD_ENDPOINT: string;
|
||||
@@ -40,9 +28,9 @@ export interface ConfigContext {
|
||||
GITHUB_TEST_ENV_CLIENT_ID: string;
|
||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||
isTerminalEnabled: boolean;
|
||||
isPhoenixEnabled: boolean;
|
||||
hostedExplorerURL: string;
|
||||
armAPIVersion?: string;
|
||||
allowedJunoOrigins: string[];
|
||||
msalRedirectURI?: string;
|
||||
}
|
||||
|
||||
@@ -52,11 +40,12 @@ let configContext: Readonly<ConfigContext> = {
|
||||
allowedParentFrameOrigins: [
|
||||
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure\\.de$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure.de$`,
|
||||
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
||||
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
|
||||
], // Webpack injects this at build time
|
||||
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`,
|
||||
],
|
||||
// Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
||||
@@ -72,7 +61,14 @@ let configContext: Readonly<ConfigContext> = {
|
||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
isTerminalEnabled: false,
|
||||
isPhoenixEnabled: false,
|
||||
allowedJunoOrigins: [
|
||||
JunoEndpoints.Test,
|
||||
JunoEndpoints.Test2,
|
||||
JunoEndpoints.Test3,
|
||||
JunoEndpoints.Prod,
|
||||
JunoEndpoints.Stage,
|
||||
"https://localhost",
|
||||
],
|
||||
};
|
||||
|
||||
export function resetConfigContext(): void {
|
||||
@@ -83,50 +79,6 @@ export function resetConfigContext(): void {
|
||||
}
|
||||
|
||||
export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
if (!newContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, allowedArmEndpoints)) {
|
||||
delete newContext.ARM_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) {
|
||||
delete newContext.AAD_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
|
||||
delete newContext.EMULATOR_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
|
||||
delete newContext.GRAPH_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.ARCADIA_ENDPOINT, allowedArcadiaEndpoints)) {
|
||||
delete newContext.ARCADIA_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.BACKEND_ENDPOINT, allowedBackendEndpoints)) {
|
||||
delete newContext.BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
|
||||
delete newContext.MONGO_BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
|
||||
delete newContext.JUNO_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.hostedExplorerURL, allowedHostedExplorerEndpoints)) {
|
||||
delete newContext.hostedExplorerURL;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.msalRedirectURI, allowedMsalRedirectEndpoints)) {
|
||||
delete newContext.msalRedirectURI;
|
||||
}
|
||||
|
||||
Object.assign(configContext, newContext);
|
||||
}
|
||||
|
||||
@@ -150,8 +102,18 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
||||
});
|
||||
if (response.status === 200) {
|
||||
try {
|
||||
const { ...externalConfig } = await response.json();
|
||||
updateConfigContext(externalConfig);
|
||||
const { allowedParentFrameOrigins, allowedJunoOrigins, ...externalConfig } = await response.json();
|
||||
Object.assign(configContext, externalConfig);
|
||||
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
|
||||
updateConfigContext({
|
||||
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins],
|
||||
});
|
||||
}
|
||||
if (allowedJunoOrigins && allowedJunoOrigins.length > 0) {
|
||||
updateConfigContext({
|
||||
allowedJunoOrigins: [...configContext.allowedJunoOrigins, ...allowedJunoOrigins],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Unable to parse json in config file");
|
||||
console.error(error);
|
||||
|
||||
@@ -450,24 +450,6 @@ export interface IResponse<T> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface IValidationError {
|
||||
message: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IMaxAllocationTimeExceeded extends IValidationError {
|
||||
earliestAllocationTimestamp: string;
|
||||
maxAllocationTimePerDayPerUserInMinutes: string;
|
||||
}
|
||||
|
||||
export interface IMaxDbAccountsPerUserExceeded extends IValidationError {
|
||||
maxSimultaneousConnectionsPerUser: string;
|
||||
}
|
||||
|
||||
export interface IMaxUsersPerDbAccountExceeded extends IValidationError {
|
||||
maxSimultaneousUsersPerDbAccount: string;
|
||||
}
|
||||
|
||||
export interface IPhoenixConnectionInfoResult {
|
||||
readonly notebookAuthToken?: string;
|
||||
readonly notebookServerUrl?: string;
|
||||
@@ -549,12 +531,3 @@ export interface ContainerConnectionInfo {
|
||||
status: ConnectionStatusType;
|
||||
//need to add ram and rom info
|
||||
}
|
||||
|
||||
export enum PhoenixErrorType {
|
||||
MaxAllocationTimeExceeded = "MaxAllocationTimeExceeded",
|
||||
MaxDbAccountsPerUserExceeded = "MaxDbAccountsPerUserExceeded",
|
||||
MaxUsersPerDbAccountExceeded = "MaxUsersPerDbAccountExceeded",
|
||||
AllocationValidationResult = "AllocationValidationResult",
|
||||
RegionNotServicable = "RegionNotServicable",
|
||||
SubscriptionNotAllowed = "SubscriptionNotAllowed",
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.offer = this.database?.offer();
|
||||
}
|
||||
|
||||
const initialState: SettingsComponentState = {
|
||||
this.state = {
|
||||
throughput: undefined,
|
||||
throughputBaseline: undefined,
|
||||
autoPilotThroughput: undefined,
|
||||
@@ -199,12 +199,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
selectedTab: SettingsV2TabTypes.ScaleTab,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
...initialState,
|
||||
...this.getBaselineValues(),
|
||||
...this.getAutoscaleBaselineValues(),
|
||||
};
|
||||
|
||||
this.saveSettingsButton = {
|
||||
isEnabled: this.isSaveSettingsButtonEnabled,
|
||||
isVisible: () => {
|
||||
@@ -231,6 +225,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.loadMongoIndexes();
|
||||
}
|
||||
|
||||
this.setAutoPilotStates();
|
||||
this.setBaseline();
|
||||
if (this.props.settingsTab.isActive()) {
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
@@ -291,24 +286,17 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
);
|
||||
};
|
||||
|
||||
private getAutoscaleBaselineValues = (): Partial<SettingsComponentState> => {
|
||||
private setAutoPilotStates = (): void => {
|
||||
const autoscaleMaxThroughput = this.offer?.autoscaleMaxThroughput;
|
||||
|
||||
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||
return {
|
||||
this.setState({
|
||||
isAutoPilotSelected: true,
|
||||
wasAutopilotOriginallySet: true,
|
||||
autoPilotThroughput: autoscaleMaxThroughput,
|
||||
autoPilotThroughputBaseline: autoscaleMaxThroughput,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isAutoPilotSelected: false,
|
||||
wasAutopilotOriginallySet: false,
|
||||
autoPilotThroughput: undefined,
|
||||
autoPilotThroughputBaseline: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
public hasProvisioningTypeChanged = (): boolean =>
|
||||
@@ -573,25 +561,21 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
};
|
||||
|
||||
public setBaseline = (): void => {
|
||||
const baselineValues = this.getBaselineValues();
|
||||
const autoscaleBaselineValues = this.getAutoscaleBaselineValues();
|
||||
this.setState({ ...baselineValues, ...autoscaleBaselineValues } as SettingsComponentState);
|
||||
};
|
||||
|
||||
private getBaselineValues = (): Partial<SettingsComponentState> => {
|
||||
const offerThroughput = this.offer?.manualThroughput;
|
||||
|
||||
if (!this.isCollectionSettingsTab) {
|
||||
return {
|
||||
this.setState({
|
||||
throughput: offerThroughput,
|
||||
throughputBaseline: offerThroughput,
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultTtl = this.collection.defaultTtl();
|
||||
|
||||
let timeToLive: TtlType;
|
||||
let timeToLiveSeconds: number;
|
||||
let timeToLive: TtlType = this.state.timeToLive;
|
||||
let timeToLiveSeconds = this.state.timeToLiveSeconds;
|
||||
switch (defaultTtl) {
|
||||
case undefined:
|
||||
case 0:
|
||||
@@ -636,7 +620,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
|
||||
const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType];
|
||||
|
||||
return {
|
||||
this.setState({
|
||||
throughput: offerThroughput,
|
||||
throughputBaseline: offerThroughput,
|
||||
changeFeedPolicy: changeFeedPolicy,
|
||||
@@ -659,7 +643,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
|
||||
geospatialConfigType: geoSpatialConfigType,
|
||||
geospatialConfigTypeBaseline: geoSpatialConfigType,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
private getTabsButtons = (): CommandButtonComponentProps[] => {
|
||||
|
||||
@@ -19,7 +19,7 @@ import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/Telemet
|
||||
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../../../UserContext";
|
||||
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
|
||||
import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils";
|
||||
import { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils";
|
||||
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
|
||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import {
|
||||
@@ -540,7 +540,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
||||
onChange={this.onAutoPilotThroughputChange}
|
||||
min={autoPilotThroughput1K}
|
||||
min={minAutoPilotThroughput}
|
||||
errorMessage={this.props.throughputError}
|
||||
/>
|
||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||
|
||||
@@ -145,7 +145,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
label="Max RU/s"
|
||||
min={1000}
|
||||
min={4000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={1000}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ const props = {
|
||||
isDatabase: false,
|
||||
showFreeTierExceedThroughputTooltip: true,
|
||||
isSharded: true,
|
||||
isFreeTier: false,
|
||||
setThroughputValue: () => jest.fn(),
|
||||
setIsAutoscale: () => jest.fn(),
|
||||
setIsThroughputCapExceeded: () => jest.fn(),
|
||||
|
||||
@@ -14,7 +14,6 @@ import "./ThroughputInput.less";
|
||||
export interface ThroughputInputProps {
|
||||
isDatabase: boolean;
|
||||
isSharded: boolean;
|
||||
isFreeTier: boolean;
|
||||
showFreeTierExceedThroughputTooltip: boolean;
|
||||
setThroughputValue: (throughput: number) => void;
|
||||
setIsAutoscale: (isAutoscale: boolean) => void;
|
||||
@@ -24,18 +23,15 @@ export interface ThroughputInputProps {
|
||||
|
||||
export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
isDatabase,
|
||||
isSharded,
|
||||
isFreeTier,
|
||||
showFreeTierExceedThroughputTooltip,
|
||||
setThroughputValue,
|
||||
setIsAutoscale,
|
||||
setIsThroughputCapExceeded,
|
||||
isSharded,
|
||||
onCostAcknowledgeChange,
|
||||
}: ThroughputInputProps) => {
|
||||
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
|
||||
const [throughput, setThroughput] = useState<number>(
|
||||
isFreeTier ? AutoPilotUtils.autoPilotThroughput1K : AutoPilotUtils.autoPilotThroughput4K
|
||||
);
|
||||
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput);
|
||||
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
||||
const [throughputError, setThroughputError] = useState<string>("");
|
||||
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
|
||||
@@ -155,14 +151,11 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
|
||||
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
|
||||
if (mode === "Autoscale") {
|
||||
const defaultThroughput = isFreeTier
|
||||
? AutoPilotUtils.autoPilotThroughput1K
|
||||
: AutoPilotUtils.autoPilotThroughput4K;
|
||||
setThroughput(defaultThroughput);
|
||||
setThroughput(AutoPilotUtils.minAutoPilotThroughput);
|
||||
setIsAutoScaleSelected(true);
|
||||
setThroughputValue(defaultThroughput);
|
||||
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
|
||||
setIsAutoscale(true);
|
||||
checkThroughputCap(defaultThroughput);
|
||||
checkThroughputCap(AutoPilotUtils.minAutoPilotThroughput);
|
||||
} else {
|
||||
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
||||
setIsAutoScaleSelected(false);
|
||||
@@ -233,7 +226,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
}}
|
||||
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
min={AutoPilotUtils.autoPilotThroughput1K}
|
||||
min={AutoPilotUtils.minAutoPilotThroughput}
|
||||
value={throughput.toString()}
|
||||
aria-label="Max request units per second"
|
||||
required={true}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
<ThroughputInput
|
||||
isDatabase={false}
|
||||
isFreeTier={false}
|
||||
isSharded={true}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
@@ -1638,7 +1637,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
aria-label="Max request units per second"
|
||||
errorMessage=""
|
||||
key=".0:$.2"
|
||||
min={1000}
|
||||
min={4000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={1000}
|
||||
@@ -1660,7 +1659,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
aria-label="Max request units per second"
|
||||
deferredValidationTime={200}
|
||||
errorMessage=""
|
||||
min={1000}
|
||||
min={4000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
resizable={true}
|
||||
@@ -1956,7 +1955,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
aria-invalid={false}
|
||||
className="ms-TextField-field field-64"
|
||||
id="TextField2"
|
||||
min={1000}
|
||||
min={4000}
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Link } from "@fluentui/react/lib/Link";
|
||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||
import { IGalleryItem } from "Juno/JunoClient";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import _ from "underscore";
|
||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import shallow from "zustand/shallow";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||
@@ -26,6 +24,7 @@ import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { useTabs } from "../hooks/useTabs";
|
||||
import { IGalleryItem } from "../Juno/JunoClient";
|
||||
import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
@@ -33,7 +32,11 @@ import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import {
|
||||
get as getWorkspace,
|
||||
listByDatabaseAccount,
|
||||
start,
|
||||
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
@@ -47,12 +50,13 @@ import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
||||
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||
import type NotebookManager from "./Notebook/NotebookManager";
|
||||
import { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||
import type { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
||||
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
||||
@@ -174,11 +178,7 @@ export default class Explorer {
|
||||
this.resourceTree = new ResourceTreeAdapter(this);
|
||||
|
||||
// Override notebook server parameters from URL parameters
|
||||
if (
|
||||
userContext.features.notebookServerUrl &&
|
||||
validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
|
||||
userContext.features.notebookServerToken
|
||||
) {
|
||||
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken,
|
||||
@@ -190,6 +190,19 @@ export default class Explorer {
|
||||
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
||||
}
|
||||
|
||||
if (userContext.features.livyEndpoint) {
|
||||
useNotebook.getState().setSparkClusterConnectionInfo({
|
||||
userName: undefined,
|
||||
password: undefined,
|
||||
endpoints: [
|
||||
{
|
||||
endpoint: userContext.features.livyEndpoint,
|
||||
kind: DataModels.SparkClusterEndpointKind.Livy,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
this.refreshExplorer();
|
||||
}
|
||||
|
||||
@@ -362,13 +375,12 @@ export default class Explorer {
|
||||
status: ConnectionStatusType.Connecting,
|
||||
};
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
let connectionInfo;
|
||||
try {
|
||||
TelemetryProcessor.traceStart(Action.PhoenixConnection, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
useNotebook.getState().setIsAllocating(true);
|
||||
connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
|
||||
const connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
|
||||
if (connectionInfo.status !== HttpStatusCodes.OK) {
|
||||
throw new Error(`Received status code: ${connectionInfo?.status}`);
|
||||
}
|
||||
@@ -387,16 +399,6 @@ export default class Explorer {
|
||||
});
|
||||
connectionStatus.status = ConnectionStatusType.Failed;
|
||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
|
||||
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
|
||||
} else {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Connection Failed",
|
||||
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket."
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
useNotebook.getState().setIsAllocating(false);
|
||||
@@ -420,10 +422,7 @@ export default class Explorer {
|
||||
connectionStatus.status = ConnectionStatusType.Connected;
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint:
|
||||
(validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
|
||||
userContext.features.notebookServerUrl) ||
|
||||
connectionInfo.data.notebookServerUrl,
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
|
||||
forwardingId: connectionInfo.data.forwardingId,
|
||||
});
|
||||
@@ -473,6 +472,35 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureNotebookWorkspaceRunning() {
|
||||
if (!userContext.databaseAccount) {
|
||||
return;
|
||||
}
|
||||
|
||||
let clearMessage;
|
||||
try {
|
||||
const notebookWorkspace = await getWorkspace(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
if (
|
||||
notebookWorkspace &&
|
||||
notebookWorkspace.properties &&
|
||||
notebookWorkspace.properties.status &&
|
||||
notebookWorkspace.properties.status.toLowerCase() === "stopped"
|
||||
) {
|
||||
clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace");
|
||||
await start(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace");
|
||||
} finally {
|
||||
clearMessage && clearMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private _resetNotebookWorkspace = async () => {
|
||||
useDialog.getState().closeDialog();
|
||||
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
|
||||
@@ -1159,12 +1187,20 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
private _openSetupNotebooksPaneForQuickstart(): void {
|
||||
const title = "Enable Notebooks (Preview)";
|
||||
const description =
|
||||
"You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(title, <SetupNoteBooksPanel explorer={this} panelTitle={title} panelDescription={description} />);
|
||||
}
|
||||
|
||||
public async handleOpenFileAction(path: string): Promise<void> {
|
||||
if (useNotebook.getState().isPhoenixNotebooks === undefined) {
|
||||
await useNotebook.getState().getPhoenixStatus();
|
||||
}
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await this.allocateContainer();
|
||||
} else if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
|
||||
this._openSetupNotebooksPaneForQuickstart();
|
||||
}
|
||||
|
||||
// We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb
|
||||
@@ -1249,10 +1285,7 @@ export default class Explorer {
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
|
||||
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||
const isNotebookEnabled =
|
||||
userContext.features.notebooksDownBanner ||
|
||||
useNotebook.getState().isPhoenixNotebooks ||
|
||||
useNotebook.getState().isPhoenixFeatures;
|
||||
const isNotebookEnabled = userContext.features.notebooksDownBanner || useNotebook.getState().isPhoenixNotebooks;
|
||||
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
||||
useNotebook
|
||||
.getState()
|
||||
|
||||
@@ -10,6 +10,7 @@ import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
|
||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||
import GitHubIcon from "../../../../images/github.svg";
|
||||
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
|
||||
import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg";
|
||||
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
||||
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
||||
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
||||
@@ -34,6 +35,7 @@ import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPa
|
||||
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
|
||||
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
|
||||
import { SetupNoteBooksPanel } from "../../Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
import { SelectedNodeState } from "../../useSelectedNode";
|
||||
|
||||
@@ -109,6 +111,11 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
buttons.push(btn);
|
||||
});
|
||||
} else {
|
||||
if (!isRunningOnNationalCloud() && useNotebook.getState().isPhoenixNotebooks) {
|
||||
buttons.push(createDivider());
|
||||
buttons.push(createEnableNotebooksButton(container));
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
||||
@@ -459,6 +466,33 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
||||
};
|
||||
}
|
||||
|
||||
function createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
return undefined;
|
||||
}
|
||||
const label = "Enable Notebooks (Preview)";
|
||||
const tooltip =
|
||||
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const description =
|
||||
"Looks like you have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
|
||||
return {
|
||||
iconSrc: EnableNotebooksIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
label,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={label} panelDescription={description} />
|
||||
),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
|
||||
ariaLabel: label,
|
||||
tooltipText: useNotebook.getState().isNotebooksEnabledForAccount ? "" : tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open Terminal";
|
||||
return {
|
||||
@@ -476,6 +510,9 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
||||
const label = "Open Mongo Shell";
|
||||
const tooltip =
|
||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const title = "Set up workspace";
|
||||
const description =
|
||||
"Looks like you have not created a workspace for this account. To proceed and start using features including mongo shell and notebook, we will need to create a default workspace in this account.";
|
||||
const disableButton =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
return {
|
||||
@@ -484,6 +521,13 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
|
||||
onCommandClick: () => {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
|
||||
} else {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
title,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
||||
);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
@@ -498,6 +542,9 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
||||
const label = "Open Cassandra Shell";
|
||||
const tooltip =
|
||||
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const title = "Set up workspace";
|
||||
const description =
|
||||
"Looks like you have not created a workspace for this account. To proceed and start using features including cassandra shell and notebook, we will need to create a default workspace in this account.";
|
||||
const disableButton =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
return {
|
||||
@@ -506,6 +553,13 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
||||
onCommandClick: () => {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||
} else {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
title,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
|
||||
);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* Notebook container related stuff
|
||||
*/
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -20,7 +19,6 @@ export class NotebookContainerClient {
|
||||
private isResettingWorkspace: boolean;
|
||||
private phoenixClient: PhoenixClient;
|
||||
private retryOptions: promiseRetry.Options;
|
||||
private scheduleTimerId: NodeJS.Timeout;
|
||||
|
||||
constructor(private onConnectionLost: () => void) {
|
||||
this.phoenixClient = new PhoenixClient();
|
||||
@@ -29,36 +27,36 @@ export class NotebookContainerClient {
|
||||
maxTimeout: Notebook.retryAttemptDelayMs,
|
||||
minTimeout: Notebook.retryAttemptDelayMs,
|
||||
};
|
||||
|
||||
this.initHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
}
|
||||
|
||||
private initHeartbeat(delayMs: number): void {
|
||||
this.scheduleHeartbeat(delayMs);
|
||||
|
||||
useNotebook.subscribe(
|
||||
() => this.scheduleHeartbeat(delayMs),
|
||||
(state) => state.notebookServerInfo
|
||||
);
|
||||
}
|
||||
|
||||
private scheduleHeartbeat(delayMs: number) {
|
||||
if (this.scheduleTimerId) {
|
||||
clearInterval(this.scheduleTimerId);
|
||||
}
|
||||
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||
this.scheduleTimerId = setInterval(async () => {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||
const memoryUsageInfo = await this.getMemoryUsage();
|
||||
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
|
||||
}
|
||||
}, delayMs);
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
} else {
|
||||
const unsub = useNotebook.subscribe(
|
||||
(newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
|
||||
if (newServerInfo?.notebookServerEndpoint) {
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
}
|
||||
unsub();
|
||||
},
|
||||
(state) => state.notebookServerInfo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Heartbeat: each ping schedules another ping
|
||||
*/
|
||||
private scheduleHeartbeat(delayMs: number): void {
|
||||
setTimeout(async () => {
|
||||
const memoryUsageInfo = await this.getMemoryUsage();
|
||||
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
}
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||
@@ -160,16 +158,6 @@ export class NotebookContainerClient {
|
||||
return null;
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
|
||||
if (error?.status === HttpStatusCodes.Forbidden && error.message) {
|
||||
useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
|
||||
} else {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Connection Failed",
|
||||
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket."
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,8 +303,8 @@ export class NotebookContentClient {
|
||||
private getServerConfig(): ServerConfig {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
return {
|
||||
endpoint: notebookServerInfo?.notebookServerEndpoint,
|
||||
token: notebookServerInfo?.authToken,
|
||||
endpoint: notebookServerInfo.notebookServerEndpoint,
|
||||
token: notebookServerInfo.authToken,
|
||||
crossDomain: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -249,7 +249,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={true}
|
||||
isSharded={this.state.isSharded}
|
||||
isFreeTier={this.isFreeTierAccount()}
|
||||
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
|
||||
@@ -484,7 +483,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={false}
|
||||
isSharded={this.state.isSharded}
|
||||
isFreeTier={this.isFreeTierAccount()}
|
||||
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
|
||||
|
||||
@@ -147,7 +147,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
if (isAutoscaleSelected) {
|
||||
if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) {
|
||||
setFormErrors(
|
||||
`Please enter a value greater than ${AutoPilotUtils.autoPilotThroughput1K} for autopilot throughput`
|
||||
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -241,7 +241,6 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
|
||||
isDatabase={true}
|
||||
isSharded={databaseCreateNewShared}
|
||||
isFreeTier={isFreeTierAccount}
|
||||
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
|
||||
|
||||
@@ -262,7 +262,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
}
|
||||
isDatabase
|
||||
isSharded
|
||||
isFreeTier={isFreeTierAccount}
|
||||
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
|
||||
@@ -336,7 +335,6 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
|
||||
isDatabase={false}
|
||||
isSharded
|
||||
isFreeTier={isFreeTierAccount}
|
||||
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUti
|
||||
import Explorer from "../../Explorer";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
|
||||
|
||||
@@ -103,14 +104,11 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
switch (location.type) {
|
||||
case "MyNotebooks":
|
||||
parent = {
|
||||
name: useNotebook.getState().notebookFolderName,
|
||||
name: ResourceTreeAdapter.MyNotebooksTitle,
|
||||
path: useNotebook.getState().notebookBasePath,
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
isGithubTree = false;
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
await container.allocateContainer();
|
||||
}
|
||||
break;
|
||||
|
||||
case "GitHub":
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -188,14 +188,11 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
{shouldShowCrossPartitionOption && (
|
||||
<div className="settingsSection">
|
||||
<div className="settingsSectionPart">
|
||||
<div className="settingsSectionLabel">
|
||||
Enable cross-partition query
|
||||
<InfoTooltip>
|
||||
Send more than one request while executing a query. More than one request is necessary if the query is
|
||||
not scoped to single partition key value.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
|
||||
<label className="settingsSectionLabel">Enable cross-partition query</label>
|
||||
<InfoTooltip>
|
||||
Send more than one request while executing a query. More than one request is necessary if the query is
|
||||
not scoped to single partition key value.
|
||||
</InfoTooltip>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
@@ -211,14 +208,14 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
{shouldShowParallelismOption && (
|
||||
<div className="settingsSection">
|
||||
<div className="settingsSectionPart">
|
||||
<div className="settingsSectionLabel">
|
||||
<label className="settingsSectionLabel" htmlFor="input65">
|
||||
Max degree of parallelism
|
||||
<InfoTooltip>
|
||||
Gets or sets the number of concurrent operations run client side during parallel query execution. A
|
||||
positive property value limits the number of concurrent operations to the set value. If it is set to
|
||||
less than 0, the system automatically decides the number of concurrent operations to run.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
</label>
|
||||
<InfoTooltip>
|
||||
Gets or sets the number of concurrent operations run client side during parallel query execution. A
|
||||
positive property value limits the number of concurrent operations to the set value. If it is set to
|
||||
less than 0, the system automatically decides the number of concurrent operations to run.
|
||||
</InfoTooltip>
|
||||
|
||||
<SpinButton
|
||||
min={-1}
|
||||
|
||||
@@ -103,14 +103,14 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
<div
|
||||
className="settingsSectionPart"
|
||||
>
|
||||
<div
|
||||
<label
|
||||
className="settingsSectionLabel"
|
||||
>
|
||||
Enable cross-partition query
|
||||
<InfoTooltip>
|
||||
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
</label>
|
||||
<InfoTooltip>
|
||||
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
|
||||
</InfoTooltip>
|
||||
<StyledCheckboxBase
|
||||
ariaLabel="Enable cross partition query"
|
||||
checked={true}
|
||||
@@ -132,14 +132,15 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
<div
|
||||
className="settingsSectionPart"
|
||||
>
|
||||
<div
|
||||
<label
|
||||
className="settingsSectionLabel"
|
||||
htmlFor="input65"
|
||||
>
|
||||
Max degree of parallelism
|
||||
<InfoTooltip>
|
||||
Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
</label>
|
||||
<InfoTooltip>
|
||||
Gets or sets the number of concurrent operations run client side during parallel query execution. A positive property value limits the number of concurrent operations to the set value. If it is set to less than 0, the system automatically decides the number of concurrent operations to run.
|
||||
</InfoTooltip>
|
||||
<StyledSpinButton
|
||||
ariaLabel="Max degree of parallelism"
|
||||
className="textfontclr"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { PrimaryButton } from "@fluentui/react";
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../../Explorer";
|
||||
import { SetupNoteBooksPanel } from "./SetupNotebooksPanel";
|
||||
|
||||
describe("Setup Notebooks Panel", () => {
|
||||
it("should render Default properly", () => {
|
||||
const fakeExplorer = {} as Explorer;
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
openNotificationConsole: (): void => undefined,
|
||||
panelTitle: "",
|
||||
panelDescription: "",
|
||||
};
|
||||
const wrapper = mount(<SetupNoteBooksPanel {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render button", () => {
|
||||
const fakeExplorer = {} as Explorer;
|
||||
const props = {
|
||||
explorer: fakeExplorer,
|
||||
closePanel: (): void => undefined,
|
||||
openNotificationConsole: (): void => undefined,
|
||||
panelTitle: "",
|
||||
panelDescription: "",
|
||||
};
|
||||
const wrapper = mount(<SetupNoteBooksPanel {...props} />);
|
||||
const button = wrapper.find("PrimaryButton").first();
|
||||
expect(button).toBeDefined();
|
||||
});
|
||||
|
||||
it("Button onClick should call onCompleteSetup", () => {
|
||||
const onCompleteSetupClick = jest.fn();
|
||||
const wrapper = mount(<PrimaryButton onClick={onCompleteSetupClick} />);
|
||||
wrapper.find("button").simulate("click");
|
||||
|
||||
expect(onCompleteSetupClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Button onKeyPress should call onCompleteSetupKeyPress", () => {
|
||||
const onCompleteSetupKeyPress = jest.fn();
|
||||
const wrapper = mount(<PrimaryButton onKeyPress={onCompleteSetupKeyPress} />);
|
||||
wrapper.find("button").simulate("keypress");
|
||||
|
||||
expect(onCompleteSetupKeyPress).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
121
src/Explorer/Panes/SetupNotebooksPanel/SetupNotebooksPanel.tsx
Normal file
121
src/Explorer/Panes/SetupNotebooksPanel/SetupNotebooksPanel.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { PrimaryButton } from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import React, { FunctionComponent, KeyboardEvent, useState } from "react";
|
||||
import { Areas, NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { createOrUpdate } from "../../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
||||
interface SetupNoteBooksPanelProps {
|
||||
explorer: Explorer;
|
||||
panelTitle: string;
|
||||
panelDescription: string;
|
||||
}
|
||||
|
||||
export const SetupNoteBooksPanel: FunctionComponent<SetupNoteBooksPanelProps> = ({
|
||||
explorer,
|
||||
panelTitle,
|
||||
panelDescription,
|
||||
}: SetupNoteBooksPanelProps): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
|
||||
const description = panelDescription;
|
||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
|
||||
|
||||
const onCompleteSetupClick = async () => {
|
||||
await setupNotebookWorkspace();
|
||||
};
|
||||
|
||||
const onCompleteSetupKeyPress = async (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (event.key === " " || event.key === NormalizedEventKey.Enter) {
|
||||
await setupNotebookWorkspace();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const setupNotebookWorkspace = async (): Promise<void> => {
|
||||
if (!explorer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNotebookWorkspace, {
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: panelTitle,
|
||||
});
|
||||
|
||||
const clear = NotificationConsoleUtils.logConsoleProgress("Creating a new default notebook workspace");
|
||||
|
||||
try {
|
||||
setLoadingTrue();
|
||||
await createOrUpdate(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
explorer.refreshExplorer();
|
||||
|
||||
closeSidePanel();
|
||||
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.CreateNotebookWorkspace,
|
||||
{
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: panelTitle,
|
||||
},
|
||||
startKey
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleInfo("Successfully created a default notebook workspace for the account");
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.CreateNotebookWorkspace,
|
||||
{
|
||||
dataExplorerArea: Areas.ContextualPane,
|
||||
paneTitle: panelTitle,
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
setErrorMessage(`Failed to setup a default notebook workspace: ${errorMessage}`);
|
||||
setShowErrorDetails(true);
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to create a default notebook workspace: ${errorMessage}`);
|
||||
} finally {
|
||||
setLoadingFalse();
|
||||
clear();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="panelFormWrapper">
|
||||
{errorMessage && (
|
||||
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
|
||||
)}
|
||||
<div className="panelMainContent">
|
||||
<div className="pkPadding">
|
||||
<div>{description}</div>
|
||||
<PrimaryButton
|
||||
id="completeSetupBtn"
|
||||
className="btncreatecoll1 btnSetupQueries"
|
||||
text="Complete Setup"
|
||||
onClick={onCompleteSetupClick}
|
||||
onKeyPress={onCompleteSetupKeyPress}
|
||||
aria-label="Complete setup"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && <PanelLoadingScreen />}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -35,60 +35,6 @@ const {
|
||||
Smallint,
|
||||
Tinyint,
|
||||
Timestamp,
|
||||
// List
|
||||
List_Ascii,
|
||||
List_Bigint,
|
||||
List_Blob,
|
||||
List_Boolean,
|
||||
List_Date,
|
||||
List_Decimal,
|
||||
List_Double,
|
||||
List_Float,
|
||||
List_Int,
|
||||
List_Text,
|
||||
List_Timestamp,
|
||||
List_Uuid,
|
||||
List_Varchar,
|
||||
List_Varint,
|
||||
List_Inet,
|
||||
List_Smallint,
|
||||
List_Tinyint,
|
||||
// Map
|
||||
Map_Ascii,
|
||||
Map_Bigint,
|
||||
Map_Blob,
|
||||
Map_Boolean,
|
||||
Map_Date,
|
||||
Map_Decimal,
|
||||
Map_Double,
|
||||
Map_Float,
|
||||
Map_Int,
|
||||
Map_Text,
|
||||
Map_Timestamp,
|
||||
Map_Uuid,
|
||||
Map_Varchar,
|
||||
Map_Varint,
|
||||
Map_Inet,
|
||||
Map_Smallint,
|
||||
Map_Tinyint,
|
||||
// Set
|
||||
Set_Ascii,
|
||||
Set_Bigint,
|
||||
Set_Blob,
|
||||
Set_Boolean,
|
||||
Set_Date,
|
||||
Set_Decimal,
|
||||
Set_Double,
|
||||
Set_Float,
|
||||
Set_Int,
|
||||
Set_Text,
|
||||
Set_Timestamp,
|
||||
Set_Uuid,
|
||||
Set_Varchar,
|
||||
Set_Varint,
|
||||
Set_Inet,
|
||||
Set_Smallint,
|
||||
Set_Tinyint,
|
||||
} = TableConstants.CassandraType;
|
||||
export const cassandraOptions = [
|
||||
{ key: Text, text: Text },
|
||||
@@ -108,60 +54,6 @@ export const cassandraOptions = [
|
||||
{ key: Smallint, text: Smallint },
|
||||
{ key: Tinyint, text: Tinyint },
|
||||
{ key: Timestamp, text: Timestamp },
|
||||
// List
|
||||
{ key: List_Ascii, text: List_Ascii },
|
||||
{ key: List_Bigint, text: List_Bigint },
|
||||
{ key: List_Blob, text: List_Blob },
|
||||
{ key: List_Boolean, text: List_Boolean },
|
||||
{ key: List_Date, text: List_Date },
|
||||
{ key: List_Decimal, text: List_Decimal },
|
||||
{ key: List_Double, text: List_Double },
|
||||
{ key: List_Float, text: List_Float },
|
||||
{ key: List_Int, text: List_Int },
|
||||
{ key: List_Text, text: List_Text },
|
||||
{ key: List_Timestamp, text: List_Timestamp },
|
||||
{ key: List_Uuid, text: List_Uuid },
|
||||
{ key: List_Varchar, text: List_Varchar },
|
||||
{ key: List_Varint, text: List_Varint },
|
||||
{ key: List_Inet, text: List_Inet },
|
||||
{ key: List_Smallint, text: List_Smallint },
|
||||
{ key: List_Tinyint, text: List_Tinyint },
|
||||
// Map
|
||||
{ key: Map_Ascii, text: Map_Ascii },
|
||||
{ key: Map_Bigint, text: Map_Bigint },
|
||||
{ key: Map_Blob, text: Map_Blob },
|
||||
{ key: Map_Boolean, text: Map_Boolean },
|
||||
{ key: Map_Date, text: Map_Date },
|
||||
{ key: Map_Decimal, text: Map_Decimal },
|
||||
{ key: Map_Double, text: Map_Double },
|
||||
{ key: Map_Float, text: Map_Float },
|
||||
{ key: Map_Int, text: Map_Int },
|
||||
{ key: Map_Text, text: Map_Text },
|
||||
{ key: Map_Timestamp, text: Map_Timestamp },
|
||||
{ key: Map_Uuid, text: Map_Uuid },
|
||||
{ key: Map_Varchar, text: Map_Varchar },
|
||||
{ key: Map_Varint, text: Map_Varint },
|
||||
{ key: Map_Inet, text: Map_Inet },
|
||||
{ key: Map_Smallint, text: Map_Smallint },
|
||||
{ key: Map_Tinyint, text: Map_Tinyint },
|
||||
// Set
|
||||
{ key: Set_Ascii, text: Set_Ascii },
|
||||
{ key: Set_Bigint, text: Set_Bigint },
|
||||
{ key: Set_Blob, text: Set_Blob },
|
||||
{ key: Set_Boolean, text: Set_Boolean },
|
||||
{ key: Set_Date, text: Set_Date },
|
||||
{ key: Set_Decimal, text: Set_Decimal },
|
||||
{ key: Set_Double, text: Set_Double },
|
||||
{ key: Set_Float, text: Set_Float },
|
||||
{ key: Set_Int, text: Set_Int },
|
||||
{ key: Set_Text, text: Set_Text },
|
||||
{ key: Set_Timestamp, text: Set_Timestamp },
|
||||
{ key: Set_Uuid, text: Set_Uuid },
|
||||
{ key: Set_Varchar, text: Set_Varchar },
|
||||
{ key: Set_Varint, text: Set_Varint },
|
||||
{ key: Set_Inet, text: Set_Inet },
|
||||
{ key: Set_Smallint, text: Set_Smallint },
|
||||
{ key: Set_Tinyint, text: Set_Tinyint },
|
||||
];
|
||||
|
||||
export const imageProps: IImageProps = {
|
||||
|
||||
@@ -27,60 +27,6 @@ export const CassandraType = {
|
||||
Inet: "Inet",
|
||||
Smallint: "Smallint",
|
||||
Tinyint: "Tinyint",
|
||||
|
||||
List_Ascii: "List<Ascii>",
|
||||
List_Bigint: "List<Bigint>",
|
||||
List_Blob: "List<Blob>",
|
||||
List_Boolean: "List<Boolean>",
|
||||
List_Date: "List<Date>",
|
||||
List_Decimal: "List<Decimal>",
|
||||
List_Double: "List<Double>",
|
||||
List_Float: "List<Float>",
|
||||
List_Int: "List<Int>",
|
||||
List_Text: "List<Text>",
|
||||
List_Timestamp: "List<Timestamp>",
|
||||
List_Uuid: "List<Uuid>",
|
||||
List_Varchar: "List<Varchar>",
|
||||
List_Varint: "List<Varint>",
|
||||
List_Inet: "List<Inet>",
|
||||
List_Smallint: "List<Smallint>",
|
||||
List_Tinyint: "List<Tinyint>",
|
||||
|
||||
Map_Ascii: "Map<Ascii>",
|
||||
Map_Bigint: "Map<Bigint>",
|
||||
Map_Blob: "Map<Blob>",
|
||||
Map_Boolean: "Map<Boolean>",
|
||||
Map_Date: "Map<Date>",
|
||||
Map_Decimal: "Map<Decimal>",
|
||||
Map_Double: "Map<Double>",
|
||||
Map_Float: "Map<Float>",
|
||||
Map_Int: "Map<Int>",
|
||||
Map_Text: "Map<Text>",
|
||||
Map_Timestamp: "Map<Timestamp>",
|
||||
Map_Uuid: "Map<Uuid>",
|
||||
Map_Varchar: "Map<Varchar>",
|
||||
Map_Varint: "Map<Varint>",
|
||||
Map_Inet: "Map<Inet>",
|
||||
Map_Smallint: "Map<Smallint>",
|
||||
Map_Tinyint: "Map<Tinyint>",
|
||||
|
||||
Set_Ascii: "Set<Ascii>",
|
||||
Set_Bigint: "Set<Bigint>",
|
||||
Set_Blob: "Set<Blob>",
|
||||
Set_Boolean: "Set<Boolean>",
|
||||
Set_Date: "Set<Date>",
|
||||
Set_Decimal: "Set<Decimal>",
|
||||
Set_Double: "Set<Double>",
|
||||
Set_Float: "Set<Float>",
|
||||
Set_Int: "Set<Int>",
|
||||
Set_Text: "Set<Text>",
|
||||
Set_Timestamp: "Set<Timestamp>",
|
||||
Set_Uuid: "Set<Uuid>",
|
||||
Set_Varchar: "Set<Varchar>",
|
||||
Set_Varint: "Set<Varint>",
|
||||
Set_Inet: "Set<Inet>",
|
||||
Set_Smallint: "Set<Smallint>",
|
||||
Set_Tinyint: "Set<Tinyint>",
|
||||
};
|
||||
|
||||
export const ClauseRule = {
|
||||
|
||||
@@ -215,13 +215,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
{
|
||||
metric: "Request Charge",
|
||||
value: this.state.requestChargeDisplayText,
|
||||
toolTip: "Request Charge",
|
||||
toolTip: "",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
{
|
||||
metric: "Showing Results",
|
||||
value: this.state.showingDocumentsDisplayText,
|
||||
toolTip: "Showing Results",
|
||||
toolTip: "",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -308,7 +308,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
collectionName: this.id(),
|
||||
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.rawDataModel.id + " - Items",
|
||||
tabTitle: "Items",
|
||||
});
|
||||
this.documentIds([]);
|
||||
|
||||
@@ -316,7 +316,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
partitionKey: this.partitionKey,
|
||||
documentIds: ko.observableArray<DocumentId>([]),
|
||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||
title: this.rawDataModel.id + " - Items",
|
||||
title: "Items",
|
||||
collection: this,
|
||||
node: this,
|
||||
tabPath: `${this.databaseId}>${this.id()}>Documents`,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import ko from "knockout";
|
||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import { GetGithubClientId } from "Utils/GitHubUtils";
|
||||
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
|
||||
import { configContext } from "../ConfigContext";
|
||||
@@ -485,7 +484,7 @@ export class JunoClient {
|
||||
// public for tests
|
||||
public static getJunoEndpoint(): string {
|
||||
const junoEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
||||
if (!validateEndpoint(junoEndpoint, allowedJunoOrigins)) {
|
||||
if (configContext.allowedJunoOrigins.indexOf(new URL(junoEndpoint).origin) === -1) {
|
||||
const error = `${junoEndpoint} not allowed as juno endpoint`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import {
|
||||
Areas,
|
||||
ConnectionStatusType,
|
||||
@@ -17,14 +15,9 @@ import {
|
||||
ContainerConnectionInfo,
|
||||
ContainerInfo,
|
||||
IContainerData,
|
||||
IMaxAllocationTimeExceeded,
|
||||
IMaxDbAccountsPerUserExceeded,
|
||||
IMaxUsersPerDbAccountExceeded,
|
||||
IPhoenixConnectionInfoResult,
|
||||
IProvisionData,
|
||||
IResponse,
|
||||
IValidationError,
|
||||
PhoenixErrorType,
|
||||
} from "../Contracts/DataModels";
|
||||
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -51,25 +44,23 @@ export class PhoenixClient {
|
||||
provisionData: IProvisionData,
|
||||
operation: string
|
||||
): Promise<IResponse<IPhoenixConnectionInfoResult>> {
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(`${this.getPhoenixControlPlanePathPrefix()}/containerconnections`, {
|
||||
const response = await fetch(`${this.getPhoenixControlPlanePathPrefix()}/containerconnections`, {
|
||||
method: operation === "allocate" ? "POST" : "PATCH",
|
||||
headers: PhoenixClient.getHeaders(),
|
||||
body: JSON.stringify(provisionData),
|
||||
});
|
||||
const responseJson = await response?.json();
|
||||
if (response.status === HttpStatusCodes.Forbidden) {
|
||||
throw new Error(this.ConvertToForbiddenErrorString(responseJson));
|
||||
|
||||
let data: IPhoenixConnectionInfoResult;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
}
|
||||
return {
|
||||
status: response.status,
|
||||
data: responseJson,
|
||||
data,
|
||||
};
|
||||
} catch (error) {
|
||||
if (response.status === HttpStatusCodes.Forbidden) {
|
||||
error.status = HttpStatusCodes.Forbidden;
|
||||
}
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -116,18 +107,6 @@ export class PhoenixClient {
|
||||
});
|
||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Disconnected",
|
||||
"Disconnected from temporary workspace. Please click on connect button to connect to temporary workspace."
|
||||
);
|
||||
throw new AbortError(response.statusText);
|
||||
} else if (response?.status === HttpStatusCodes.Forbidden) {
|
||||
const validationMessage = this.ConvertToForbiddenErrorString(await response.json());
|
||||
if (validationMessage) {
|
||||
useDialog.getState().showOkModalDialog("Connection Failed", `${validationMessage}`);
|
||||
}
|
||||
throw new AbortError(response.statusText);
|
||||
}
|
||||
throw new Error(response.statusText);
|
||||
@@ -175,7 +154,7 @@ export class PhoenixClient {
|
||||
public static getPhoenixEndpoint(): string {
|
||||
const phoenixEndpoint =
|
||||
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
||||
if (!validateEndpoint(phoenixEndpoint, allowedJunoOrigins)) {
|
||||
if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) {
|
||||
const error = `${phoenixEndpoint} not allowed as juno endpoint`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
@@ -197,48 +176,4 @@ export class PhoenixClient {
|
||||
[HttpHeaders.contentType]: "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
public ConvertToForbiddenErrorString(jsonData: IValidationError): string {
|
||||
const errInfo = jsonData;
|
||||
switch (errInfo?.type) {
|
||||
case PhoenixErrorType.MaxAllocationTimeExceeded: {
|
||||
const maxAllocationTimeExceeded = errInfo as IMaxAllocationTimeExceeded;
|
||||
const allocateAfterTimestamp = new Date(maxAllocationTimeExceeded?.earliestAllocationTimestamp);
|
||||
allocateAfterTimestamp.setDate(allocateAfterTimestamp.getDate() + 1);
|
||||
return (
|
||||
`${errInfo.message}` +
|
||||
" Max allocation time for a day to a user is " +
|
||||
`${maxAllocationTimeExceeded.maxAllocationTimePerDayPerUserInMinutes}` +
|
||||
". Please try again after " +
|
||||
`${allocateAfterTimestamp.toLocaleString()}`
|
||||
);
|
||||
}
|
||||
case PhoenixErrorType.MaxDbAccountsPerUserExceeded: {
|
||||
const maxDbAccountsPerUserExceeded = errInfo as IMaxDbAccountsPerUserExceeded;
|
||||
return (
|
||||
`${errInfo.message}` +
|
||||
" Max simultaneous connections allowed per user is " +
|
||||
`${maxDbAccountsPerUserExceeded.maxSimultaneousConnectionsPerUser}` +
|
||||
"."
|
||||
);
|
||||
}
|
||||
case PhoenixErrorType.MaxUsersPerDbAccountExceeded: {
|
||||
const maxUsersPerDbAccountExceeded = errInfo as IMaxUsersPerDbAccountExceeded;
|
||||
return (
|
||||
`${errInfo.message}` +
|
||||
" Max simultaneous users allowed per DbAccount is " +
|
||||
`${maxUsersPerDbAccountExceeded.maxSimultaneousUsersPerDbAccount}` +
|
||||
"."
|
||||
);
|
||||
}
|
||||
case PhoenixErrorType.AllocationValidationResult:
|
||||
case PhoenixErrorType.RegionNotServicable:
|
||||
case PhoenixErrorType.SubscriptionNotAllowed: {
|
||||
return `${errInfo.message}`;
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export type Features = {
|
||||
// set only via feature flags
|
||||
readonly canExceedMaximumValue: boolean;
|
||||
readonly cosmosdb: boolean;
|
||||
readonly enableChangeFeedPolicy: boolean;
|
||||
@@ -9,6 +8,12 @@ export type Features = {
|
||||
readonly enableReactPane: boolean;
|
||||
readonly enableRightPanelV2: boolean;
|
||||
readonly enableSchema: boolean;
|
||||
autoscaleDefault: boolean;
|
||||
partitionKeyDefault: boolean;
|
||||
partitionKeyDefault2: boolean;
|
||||
phoenixNotebooks: boolean;
|
||||
phoenixFeatures: boolean;
|
||||
notebooksDownBanner: boolean;
|
||||
readonly enableSDKoperations: boolean;
|
||||
readonly enableSpark: boolean;
|
||||
readonly enableTtl: boolean;
|
||||
@@ -18,6 +23,7 @@ export type Features = {
|
||||
readonly hostedDataExplorer: boolean;
|
||||
readonly junoEndpoint?: string;
|
||||
readonly phoenixEndpoint?: string;
|
||||
readonly livyEndpoint?: string;
|
||||
readonly notebookBasePath?: string;
|
||||
readonly notebookServerToken?: string;
|
||||
readonly notebookServerUrl?: string;
|
||||
@@ -29,21 +35,11 @@ export type Features = {
|
||||
readonly mongoProxyEndpoint?: string;
|
||||
readonly mongoProxyAPIs?: string;
|
||||
readonly enableThroughputCap: boolean;
|
||||
|
||||
// can be set via both flight and feature flag
|
||||
autoscaleDefault: boolean;
|
||||
partitionKeyDefault: boolean;
|
||||
partitionKeyDefault2: boolean;
|
||||
phoenixNotebooks?: boolean;
|
||||
phoenixFeatures?: boolean;
|
||||
notebooksDownBanner: boolean;
|
||||
};
|
||||
|
||||
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features {
|
||||
const downcased = new URLSearchParams();
|
||||
const set = (value: string, key: string) => {
|
||||
downcased.set(key.toLowerCase(), value);
|
||||
};
|
||||
const set = (value: string, key: string) => downcased.set(key.toLowerCase(), value);
|
||||
const get = (key: string, defaultValue?: string) =>
|
||||
downcased.get("feature." + key) ?? downcased.get(key) ?? defaultValue;
|
||||
|
||||
@@ -76,6 +72,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
mongoProxyAPIs: get("mongoproxyapis"),
|
||||
junoEndpoint: get("junoendpoint"),
|
||||
phoenixEndpoint: get("phoenixendpoint"),
|
||||
livyEndpoint: get("livyendpoint"),
|
||||
notebookBasePath: get("notebookbasepath"),
|
||||
notebookServerToken: get("notebookservertoken"),
|
||||
notebookServerUrl: get("notebookserverurl"),
|
||||
@@ -87,6 +84,8 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
autoscaleDefault: "true" === get("autoscaledefault"),
|
||||
partitionKeyDefault: "true" === get("partitionkeytest"),
|
||||
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
|
||||
phoenixNotebooks: "true" === get("phoenixnotebooks"),
|
||||
phoenixFeatures: "true" === get("phoenixfeatures"),
|
||||
notebooksDownBanner: "true" === get("notebooksDownBanner"),
|
||||
enableThroughputCap: "true" === get("enablethroughputcap"),
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export const autoPilotThroughput1K = 1000;
|
||||
export const minAutoPilotThroughput = 4000;
|
||||
|
||||
export const autoPilotIncrementStep = 1000;
|
||||
export const autoPilotThroughput4K = 4000;
|
||||
|
||||
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
||||
if (!maxThroughput) {
|
||||
return false;
|
||||
}
|
||||
if (maxThroughput < autoPilotThroughput1K) {
|
||||
if (maxThroughput < minAutoPilotThroughput) {
|
||||
return false;
|
||||
}
|
||||
if (maxThroughput % 1000) {
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { JunoEndpoints } from "Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
|
||||
export function validateEndpoint(
|
||||
endpointToValidate: string | undefined,
|
||||
allowedEndpoints: ReadonlyArray<string>
|
||||
): boolean {
|
||||
try {
|
||||
return validateEndpointInternal(
|
||||
endpointToValidate,
|
||||
allowedEndpoints.map((e) => e)
|
||||
);
|
||||
} catch (reason) {
|
||||
Logger.logError(`${endpointToValidate} not allowed`, "validateEndpoint");
|
||||
Logger.logError(`${JSON.stringify(reason)}`, "validateEndpoint");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function validateEndpointInternal(
|
||||
endpointToValidate: string | undefined,
|
||||
allowedEndpoints: ReadonlyArray<string>
|
||||
): boolean {
|
||||
if (endpointToValidate === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const originToValidate: string = new URL(endpointToValidate).origin;
|
||||
const allowedOrigins: string[] = allowedEndpoints.map((allowedEndpoint) => new URL(allowedEndpoint).origin) || [];
|
||||
const valid = allowedOrigins.indexOf(originToValidate) >= 0;
|
||||
|
||||
if (!valid) {
|
||||
throw new Error(
|
||||
`${endpointToValidate} is not an allowed endpoint. Allowed endpoints are ${allowedEndpoints.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
export const allowedArmEndpoints: ReadonlyArray<string> = [
|
||||
"https://management.azure.com",
|
||||
"https://management.usgovcloudapi.net",
|
||||
"https://management.chinacloudapi.cn",
|
||||
];
|
||||
|
||||
export const allowedAadEndpoints: ReadonlyArray<string> = ["https://login.microsoftonline.com/"];
|
||||
|
||||
export const allowedBackendEndpoints: ReadonlyArray<string> = [
|
||||
"https://main.documentdb.ext.azure.com",
|
||||
"https://main.documentdb.ext.azure.cn",
|
||||
"https://main.documentdb.ext.azure.us",
|
||||
"https://localhost:12901",
|
||||
"https://localhost:1234",
|
||||
];
|
||||
|
||||
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
|
||||
"https://main.documentdb.ext.azure.com",
|
||||
"https://main.documentdb.ext.azure.cn",
|
||||
"https://main.documentdb.ext.azure.us",
|
||||
"https://localhost:12901",
|
||||
];
|
||||
|
||||
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
|
||||
|
||||
export const allowedMongoBackendEndpoints: ReadonlyArray<string> = ["https://localhost:1234"];
|
||||
|
||||
export const allowedGraphEndpoints: ReadonlyArray<string> = ["https://graph.windows.net"];
|
||||
|
||||
export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspaceartifacts.projectarcadia.net"];
|
||||
|
||||
export const allowedHostedExplorerEndpoints: ReadonlyArray<string> = ["https://cosmos.azure.com/"];
|
||||
|
||||
export const allowedMsalRedirectEndpoints: ReadonlyArray<string> = [
|
||||
"https://cosmos-explorer-preview.azurewebsites.net/",
|
||||
];
|
||||
|
||||
export const allowedJunoOrigins: ReadonlyArray<string> = [
|
||||
JunoEndpoints.Test,
|
||||
JunoEndpoints.Test2,
|
||||
JunoEndpoints.Test3,
|
||||
JunoEndpoints.Prod,
|
||||
JunoEndpoints.Stage,
|
||||
"https://localhost",
|
||||
];
|
||||
|
||||
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
|
||||
@@ -2,28 +2,26 @@ import { isInvalidParentFrameOrigin, isReadyMessage } from "./MessageValidation"
|
||||
|
||||
describe("isInvalidParentFrameOrigin", () => {
|
||||
test.each`
|
||||
domain | expected
|
||||
${"https://cosmos.azure.com"} | ${false}
|
||||
${"https://cosmos.azure.us"} | ${false}
|
||||
${"https://cosmos.azure.cn"} | ${false}
|
||||
${"https://portal.azure.com"} | ${false}
|
||||
${"https://portal.azure.us"} | ${false}
|
||||
${"https://portal.azure.cn"} | ${false}
|
||||
${"https://portal.microsoftazure.de"} | ${false}
|
||||
${"https://subdomain.portal.azure.com"} | ${false}
|
||||
${"https://subdomain.portal.azure.us"} | ${false}
|
||||
${"https://subdomain.portal.azure.cn"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.com"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.us"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.cn"} | ${false}
|
||||
${"https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de"} | ${false}
|
||||
${"https://main.documentdb.ext.microsoftazure.de"} | ${false}
|
||||
${"https://random.domain"} | ${true}
|
||||
${"https://malicious.cloudapp.azure.com"} | ${true}
|
||||
${"https://malicious.germanycentral.cloudapp.microsoftazure.de"} | ${true}
|
||||
${"https://maliciousazure.com"} | ${true}
|
||||
${"https://maliciousportalsazure.com"} | ${true}
|
||||
${"https://cosmos-db-dataexplorer-germanycentralAazurewebsites.de"} | ${true}
|
||||
domain | expected
|
||||
${"https://cosmos.azure.com"} | ${false}
|
||||
${"https://cosmos.azure.us"} | ${false}
|
||||
${"https://cosmos.azure.cn"} | ${false}
|
||||
${"https://portal.azure.com"} | ${false}
|
||||
${"https://portal.azure.us"} | ${false}
|
||||
${"https://portal.azure.cn"} | ${false}
|
||||
${"https://portal.microsoftazure.de"} | ${false}
|
||||
${"https://subdomain.portal.azure.com"} | ${false}
|
||||
${"https://subdomain.portal.azure.us"} | ${false}
|
||||
${"https://subdomain.portal.azure.cn"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.com"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.us"} | ${false}
|
||||
${"https://main.documentdb.ext.azure.cn"} | ${false}
|
||||
${"https://main.documentdb.ext.microsoftazure.de"} | ${false}
|
||||
${"https://random.domain"} | ${true}
|
||||
${"https://malicious.cloudapp.azure.com"} | ${true}
|
||||
${"https://malicious.germanycentral.cloudapp.microsoftazure.de"} | ${true}
|
||||
${"https://maliciousazure.com"} | ${true}
|
||||
${"https://maliciousportalsazure.com"} | ${true}
|
||||
`("returns $expected when called with $domain", ({ domain, expected }) => {
|
||||
expect(isInvalidParentFrameOrigin({ origin: domain } as MessageEvent)).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ export function isInvalidParentFrameOrigin(event: MessageEvent): boolean {
|
||||
return !isValidOrigin(configContext.allowedParentFrameOrigins, event);
|
||||
}
|
||||
|
||||
function isValidOrigin(allowedOrigins: ReadonlyArray<string>, event: MessageEvent): boolean {
|
||||
function isValidOrigin(allowedOrigins: string[], event: MessageEvent): boolean {
|
||||
const eventOrigin = (event && event.origin) || "";
|
||||
const windowOrigin = (window && window.origin) || "";
|
||||
if (eventOrigin === windowOrigin) {
|
||||
|
||||
@@ -351,11 +351,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
if (inputs.features) {
|
||||
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
|
||||
}
|
||||
//Updating phoenix feature flags for MPAC based of config context
|
||||
if (configContext.isPhoenixEnabled === true) {
|
||||
userContext.features.phoenixNotebooks = true;
|
||||
userContext.features.phoenixFeatures = true;
|
||||
}
|
||||
if (inputs.flights) {
|
||||
if (inputs.flights.indexOf(Flights.AutoscaleTest) !== -1) {
|
||||
userContext.features.autoscaleDefault;
|
||||
|
||||
Reference in New Issue
Block a user