mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 02:41:39 +00:00
Compare commits
10 Commits
fix_a11y_D
...
languy-add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cc98f0b62 | ||
|
|
5c8016ecd6 | ||
|
|
605117c62d | ||
|
|
7a809cd2bc | ||
|
|
0a51e24b94 | ||
|
|
f36a881679 | ||
|
|
b7f0548cca | ||
|
|
4728dc48d7 | ||
|
|
9358fd5889 | ||
|
|
f5da8bb276 |
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@@ -12,7 +12,8 @@
|
||||
"--inspect-brk",
|
||||
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||
"--runInBand",
|
||||
"--coverage", "false"
|
||||
"--coverage",
|
||||
"false"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
@@ -26,7 +27,8 @@
|
||||
"--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,4 +1,5 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
|
||||
"isTerminalEnabled" : true
|
||||
"isTerminalEnabled" : true,
|
||||
"isPhoenixEnabled" : true
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||
"isTerminalEnabled" : false
|
||||
"isTerminalEnabled" : false,
|
||||
"isPhoenixEnabled" : false
|
||||
}
|
||||
|
||||
29708
package-lock.json
generated
29708
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -86,6 +86,7 @@
|
||||
"react-dnd": "14.0.2",
|
||||
"react-dnd-html5-backend": "14.0.0",
|
||||
"react-dom": "16.13.1",
|
||||
"react-dropzone": "12.0.4",
|
||||
"react-hotkeys": "2.0.0",
|
||||
"react-i18next": "11.8.5",
|
||||
"react-notification-system": "0.2.17",
|
||||
|
||||
@@ -99,6 +99,7 @@ export class Flights {
|
||||
public static readonly PhoenixNotebooks = "phoenixnotebooks";
|
||||
public static readonly PhoenixFeatures = "phoenixfeatures";
|
||||
public static readonly NotebooksDownBanner = "notebooksdownbanner";
|
||||
public static readonly FreeTierAutoscaleThroughput = "freetierautoscalethroughput";
|
||||
}
|
||||
|
||||
export class AfecFeatures {
|
||||
|
||||
@@ -236,13 +236,12 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
|
||||
it("returns a production endpoint", () => {
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||
});
|
||||
|
||||
it("returns a development endpoint", () => {
|
||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint("https://localhost:1234");
|
||||
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
|
||||
});
|
||||
|
||||
@@ -250,7 +249,7 @@ describe("MongoProxyClient", () => {
|
||||
updateUserContext({
|
||||
authType: AuthType.EncryptedToken,
|
||||
});
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
@@ -336,14 +337,17 @@ export function createMongoCollectionWithProxy(
|
||||
}
|
||||
|
||||
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||
return hasFlag(userContext.features.mongoProxyAPIs, feature)
|
||||
? getEndpoint(userContext.features.mongoProxyEndpoint)
|
||||
: getEndpoint();
|
||||
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);
|
||||
}
|
||||
|
||||
export function getEndpoint(customEndpoint?: string): string {
|
||||
let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
url += "/api/mongo/explorer";
|
||||
export function getEndpoint(endpoint: string): string {
|
||||
let url = endpoint + "/api/mongo/explorer";
|
||||
|
||||
if (userContext.authType === AuthType.EncryptedToken) {
|
||||
url = url.replace("api/mongo", "api/guest/mongo");
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
import { JunoEndpoints } from "Common/Constants";
|
||||
import {
|
||||
allowedAadEndpoints,
|
||||
allowedArcadiaEndpoints,
|
||||
allowedArmEndpoints,
|
||||
allowedBackendEndpoints,
|
||||
allowedEmulatorEndpoints,
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMongoBackendEndpoints,
|
||||
allowedMsalRedirectEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointValidation";
|
||||
|
||||
export enum Platform {
|
||||
Portal = "Portal",
|
||||
@@ -8,7 +20,7 @@ export enum Platform {
|
||||
|
||||
export interface ConfigContext {
|
||||
platform: Platform;
|
||||
allowedParentFrameOrigins: string[];
|
||||
allowedParentFrameOrigins: ReadonlyArray<string>;
|
||||
gitSha?: string;
|
||||
proxyPath?: string;
|
||||
AAD_ENDPOINT: string;
|
||||
@@ -28,9 +40,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;
|
||||
}
|
||||
|
||||
@@ -44,8 +56,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
`^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
|
||||
], // Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
||||
@@ -61,14 +72,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
isTerminalEnabled: false,
|
||||
allowedJunoOrigins: [
|
||||
JunoEndpoints.Test,
|
||||
JunoEndpoints.Test2,
|
||||
JunoEndpoints.Test3,
|
||||
JunoEndpoints.Prod,
|
||||
JunoEndpoints.Stage,
|
||||
"https://localhost",
|
||||
],
|
||||
isPhoenixEnabled: false,
|
||||
};
|
||||
|
||||
export function resetConfigContext(): void {
|
||||
@@ -79,6 +83,50 @@ 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);
|
||||
}
|
||||
|
||||
@@ -102,18 +150,8 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
||||
});
|
||||
if (response.status === 200) {
|
||||
try {
|
||||
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],
|
||||
});
|
||||
}
|
||||
const { ...externalConfig } = await response.json();
|
||||
updateConfigContext(externalConfig);
|
||||
} catch (error) {
|
||||
console.error("Unable to parse json in config file");
|
||||
console.error(error);
|
||||
|
||||
@@ -9,6 +9,7 @@ export enum TabKind {
|
||||
Graph,
|
||||
SQLQuery,
|
||||
ScaleSettings,
|
||||
DataUploader,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -142,6 +142,7 @@ export interface Collection extends CollectionBase {
|
||||
onGraphDocumentsClick(): void;
|
||||
onMongoDBDocumentsClick(): void;
|
||||
onSchemaAnalyzerClick(): void;
|
||||
onDataUploaderClick(): void;
|
||||
openTab(): void;
|
||||
|
||||
onSettingsClick: () => Promise<void>;
|
||||
@@ -364,6 +365,7 @@ export enum CollectionTabKind {
|
||||
CollectionSettingsV2 = 20,
|
||||
DatabaseSettingsV2 = 21,
|
||||
SchemaAnalyzer = 22,
|
||||
DataUploader = 23,
|
||||
}
|
||||
|
||||
export enum TerminalKind {
|
||||
|
||||
@@ -49,6 +49,7 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
onClick={this.toggleCollapsed}
|
||||
onKeyPress={this.onKeyPress}
|
||||
tabIndex={0}
|
||||
aria-name="Advanced"
|
||||
role="button"
|
||||
aria-expanded={this.state.isExpanded}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`CollapsibleSectionComponent renders 1`] = `
|
||||
<Fragment>
|
||||
<Stack
|
||||
aria-expanded={true}
|
||||
aria-name="Advanced"
|
||||
className="collapsibleSection"
|
||||
horizontal={true}
|
||||
onClick={[Function]}
|
||||
|
||||
@@ -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 { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils";
|
||||
import { autoPilotThroughput1K, autoPilotThroughput4K } 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={minAutoPilotThroughput}
|
||||
min={userContext.features.freetierAutoscaleThroughput ? autoPilotThroughput1K : autoPilotThroughput4K}
|
||||
errorMessage={this.props.throughputError}
|
||||
/>
|
||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ const props = {
|
||||
isDatabase: false,
|
||||
showFreeTierExceedThroughputTooltip: true,
|
||||
isSharded: true,
|
||||
isFreeTier: false,
|
||||
setThroughputValue: () => jest.fn(),
|
||||
setIsAutoscale: () => jest.fn(),
|
||||
setIsThroughputCapExceeded: () => jest.fn(),
|
||||
|
||||
@@ -14,6 +14,7 @@ import "./ThroughputInput.less";
|
||||
export interface ThroughputInputProps {
|
||||
isDatabase: boolean;
|
||||
isSharded: boolean;
|
||||
isFreeTier: boolean;
|
||||
showFreeTierExceedThroughputTooltip: boolean;
|
||||
setThroughputValue: (throughput: number) => void;
|
||||
setIsAutoscale: (isAutoscale: boolean) => void;
|
||||
@@ -23,15 +24,20 @@ 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>(AutoPilotUtils.minAutoPilotThroughput);
|
||||
const [throughput, setThroughput] = useState<number>(
|
||||
isFreeTier && userContext.features.freetierAutoscaleThroughput
|
||||
? AutoPilotUtils.autoPilotThroughput1K
|
||||
: AutoPilotUtils.autoPilotThroughput4K
|
||||
);
|
||||
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
|
||||
const [throughputError, setThroughputError] = useState<string>("");
|
||||
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
|
||||
@@ -151,11 +157,15 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
|
||||
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
|
||||
if (mode === "Autoscale") {
|
||||
setThroughput(AutoPilotUtils.minAutoPilotThroughput);
|
||||
const defaultThroughput =
|
||||
isFreeTier && userContext.features.freetierAutoscaleThroughput
|
||||
? AutoPilotUtils.autoPilotThroughput1K
|
||||
: AutoPilotUtils.autoPilotThroughput4K;
|
||||
setThroughput(defaultThroughput);
|
||||
setIsAutoScaleSelected(true);
|
||||
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
|
||||
setThroughputValue(defaultThroughput);
|
||||
setIsAutoscale(true);
|
||||
checkThroughputCap(AutoPilotUtils.minAutoPilotThroughput);
|
||||
checkThroughputCap(defaultThroughput);
|
||||
} else {
|
||||
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
|
||||
setIsAutoScaleSelected(false);
|
||||
@@ -226,7 +236,11 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
}}
|
||||
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
min={AutoPilotUtils.minAutoPilotThroughput}
|
||||
min={
|
||||
userContext.features.freetierAutoscaleThroughput
|
||||
? AutoPilotUtils.autoPilotThroughput1K
|
||||
: AutoPilotUtils.autoPilotThroughput4K
|
||||
}
|
||||
value={throughput.toString()}
|
||||
aria-label="Max request units per second"
|
||||
required={true}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
<ThroughputInput
|
||||
isDatabase={false}
|
||||
isFreeTier={false}
|
||||
isSharded={true}
|
||||
onCostAcknowledgeChange={[Function]}
|
||||
setIsAutoscale={[Function]}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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";
|
||||
@@ -24,7 +26,6 @@ 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";
|
||||
@@ -32,11 +33,7 @@ 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 {
|
||||
get as getWorkspace,
|
||||
listByDatabaseAccount,
|
||||
start,
|
||||
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
||||
@@ -50,13 +47,12 @@ 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 type { NotebookPaneContent } from "./Notebook/NotebookManager";
|
||||
import { 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";
|
||||
@@ -178,7 +174,11 @@ export default class Explorer {
|
||||
this.resourceTree = new ResourceTreeAdapter(this);
|
||||
|
||||
// Override notebook server parameters from URL parameters
|
||||
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) {
|
||||
if (
|
||||
userContext.features.notebookServerUrl &&
|
||||
validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
|
||||
userContext.features.notebookServerToken
|
||||
) {
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken,
|
||||
@@ -190,19 +190,6 @@ 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();
|
||||
}
|
||||
|
||||
@@ -399,6 +386,12 @@ export default class Explorer {
|
||||
});
|
||||
connectionStatus.status = ConnectionStatusType.Failed;
|
||||
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||
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);
|
||||
@@ -422,7 +415,10 @@ export default class Explorer {
|
||||
connectionStatus.status = ConnectionStatusType.Connected;
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
|
||||
notebookServerEndpoint:
|
||||
(validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
|
||||
userContext.features.notebookServerUrl) ||
|
||||
connectionInfo.data.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
|
||||
forwardingId: connectionInfo.data.forwardingId,
|
||||
});
|
||||
@@ -472,35 +468,6 @@ 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");
|
||||
@@ -1187,20 +1154,12 @@ 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
|
||||
@@ -1285,7 +1244,10 @@ export default class Explorer {
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
|
||||
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||
const isNotebookEnabled = userContext.features.notebooksDownBanner || useNotebook.getState().isPhoenixNotebooks;
|
||||
const isNotebookEnabled =
|
||||
userContext.features.notebooksDownBanner ||
|
||||
useNotebook.getState().isPhoenixNotebooks ||
|
||||
useNotebook.getState().isPhoenixFeatures;
|
||||
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
|
||||
useNotebook
|
||||
.getState()
|
||||
|
||||
@@ -10,7 +10,6 @@ import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
|
||||
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
|
||||
import GitHubIcon from "../../../../images/github.svg";
|
||||
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
|
||||
import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg";
|
||||
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
|
||||
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
|
||||
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
||||
@@ -35,7 +34,6 @@ 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";
|
||||
|
||||
@@ -111,11 +109,6 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
buttons.push(btn);
|
||||
});
|
||||
} else {
|
||||
if (!isRunningOnNationalCloud() && useNotebook.getState().isPhoenixNotebooks) {
|
||||
buttons.push(createDivider());
|
||||
buttons.push(createEnableNotebooksButton(container));
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
|
||||
@@ -466,33 +459,6 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
|
||||
};
|
||||
}
|
||||
|
||||
function createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
return undefined;
|
||||
}
|
||||
const label = "Enable Notebooks (Preview)";
|
||||
const tooltip =
|
||||
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
|
||||
const description =
|
||||
"Looks like you have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
|
||||
return {
|
||||
iconSrc: EnableNotebooksIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
label,
|
||||
<SetupNoteBooksPanel explorer={container} panelTitle={label} panelDescription={description} />
|
||||
),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
|
||||
ariaLabel: label,
|
||||
tooltipText: useNotebook.getState().isNotebooksEnabledForAccount ? "" : tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open Terminal";
|
||||
return {
|
||||
@@ -510,9 +476,6 @@ 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 {
|
||||
@@ -521,13 +484,6 @@ 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,
|
||||
@@ -542,9 +498,6 @@ 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 {
|
||||
@@ -553,13 +506,6 @@ 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,
|
||||
|
||||
5
src/Explorer/Notebook/DataUploader/DataUploader.less
Normal file
5
src/Explorer/Notebook/DataUploader/DataUploader.less
Normal file
@@ -0,0 +1,5 @@
|
||||
.dataUploader {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
209
src/Explorer/Notebook/DataUploader/DataUploader.tsx
Normal file
209
src/Explorer/Notebook/DataUploader/DataUploader.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import { ImmutableOutput } from "@nteract/commutable";
|
||||
import { actions, AppState, ContentRef, KernelRef, selectors } from "@nteract/core";
|
||||
import Immutable from "immutable";
|
||||
import React, { CSSProperties, useCallback, useMemo } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
import "./DataUploader.less";
|
||||
|
||||
interface DataUploaderPureProps {
|
||||
contentRef: ContentRef;
|
||||
kernelRef: KernelRef;
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
}
|
||||
|
||||
const getColor = (props) => {
|
||||
if (props.isDragAccept) {
|
||||
return "#00e676";
|
||||
}
|
||||
if (props.isDragReject) {
|
||||
return "#ff1744";
|
||||
}
|
||||
if (props.isFocused) {
|
||||
return "#2196f3";
|
||||
}
|
||||
return "#eeeeee";
|
||||
};
|
||||
|
||||
interface DataUploaderDispatchProps {
|
||||
runCell: (contentRef: ContentRef, cellId: string) => void;
|
||||
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
|
||||
updateCell: (text: string, id: string, contentRef: ContentRef) => void;
|
||||
}
|
||||
|
||||
type DataUploaderProps = DataUploaderPureProps & StateProps & DataUploaderDispatchProps;
|
||||
|
||||
const DataUploader: React.FC<DataUploaderProps> = (props) => {
|
||||
// componentDidMount(): void {
|
||||
// loadTransform(this.props);
|
||||
// }
|
||||
|
||||
// private onAnalyzeButtonClick = (filter: string = DefaultFilter, sampleSize: string = this.state.sampleSize) => {
|
||||
// const query = {
|
||||
// command: "listSchema",
|
||||
// database: this.props.databaseId,
|
||||
// collection: this.props.collectionId,
|
||||
// outputType: this.state.outputType,
|
||||
// filter,
|
||||
// sampleSize,
|
||||
// };
|
||||
|
||||
// this.setState({
|
||||
// isFiltering: true,
|
||||
// });
|
||||
|
||||
// this.props.updateCell(JSON.stringify(query), this.props.firstCellId, this.props.contentRef);
|
||||
|
||||
// this.clickAnalyzeTelemetryStartKey = traceStart(Action.DataUploaderClickAnalyze, {
|
||||
// database: this.props.databaseId,
|
||||
// collection: this.props.collectionId,
|
||||
// sampleSize,
|
||||
// });
|
||||
|
||||
// this.props.runCell(this.props.contentRef, this.props.firstCellId);
|
||||
// };
|
||||
|
||||
const { firstCellId: id, contentRef, kernelStatus } = props;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("firstCellId: id, contentRef, kernelStatus", id, contentRef, kernelStatus);
|
||||
|
||||
const isKernelBusy = kernelStatus === "busy";
|
||||
const isKernelIdle = kernelStatus === "idle";
|
||||
|
||||
const baseStyle: CSSProperties = {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
padding: "20px",
|
||||
borderWidth: 2,
|
||||
borderRadius: 2,
|
||||
borderColor: "#eeeeee",
|
||||
borderStyle: "dashed",
|
||||
backgroundColor: "#fafafa",
|
||||
color: "#bdbdbd",
|
||||
transition: "border .3s ease-in-out",
|
||||
};
|
||||
|
||||
const activeStyle = {
|
||||
borderColor: "#2196f3",
|
||||
};
|
||||
|
||||
const acceptStyle = {
|
||||
borderColor: "#00e676",
|
||||
};
|
||||
|
||||
const rejectStyle = {
|
||||
borderColor: "#ff1744",
|
||||
};
|
||||
|
||||
const onDrop = useCallback((acceptedFiles) => {
|
||||
//eslint-disable-next-line no-console
|
||||
console.log("acceptedFiles", acceptedFiles);
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject } = useDropzone({
|
||||
onDrop,
|
||||
accept: ".json",
|
||||
});
|
||||
|
||||
const style = useMemo(
|
||||
() => ({
|
||||
...baseStyle,
|
||||
...(isDragActive ? activeStyle : {}),
|
||||
...(isDragAccept ? acceptStyle : {}),
|
||||
...(isDragReject ? rejectStyle : {}),
|
||||
}),
|
||||
[isDragActive, isDragReject, isDragAccept]
|
||||
);
|
||||
|
||||
if (!id) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...getRootProps({ style })}>
|
||||
<input {...getInputProps()} />
|
||||
<div>Drag and drop your json file here</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface StateProps {
|
||||
firstCellId: string;
|
||||
kernelStatus: string;
|
||||
outputs: Immutable.List<ImmutableOutput>;
|
||||
}
|
||||
|
||||
interface InitialProps {
|
||||
kernelRef: string;
|
||||
contentRef: string;
|
||||
}
|
||||
|
||||
// Redux
|
||||
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
|
||||
const { kernelRef, contentRef } = initialProps;
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
let kernelStatus;
|
||||
let firstCellId;
|
||||
let outputs;
|
||||
|
||||
const kernel = selectors.kernel(state, { kernelRef });
|
||||
if (kernel) {
|
||||
kernelStatus = kernel.status;
|
||||
}
|
||||
|
||||
const content = selectors.content(state, { contentRef });
|
||||
if (content?.type === "notebook") {
|
||||
const cellOrder = selectors.notebook.cellOrder(content.model);
|
||||
if (cellOrder.size > 0) {
|
||||
firstCellId = cellOrder.first() as string;
|
||||
|
||||
const model = selectors.model(state, { contentRef });
|
||||
if (model && model.type === "notebook") {
|
||||
const cell = selectors.notebook.cellById(model, { id: firstCellId });
|
||||
if (cell) {
|
||||
outputs = cell.get("outputs", Immutable.List());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
firstCellId,
|
||||
kernelStatus,
|
||||
outputs,
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const makeMapDispatchToProps = () => {
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => {
|
||||
return {
|
||||
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
|
||||
return dispatch(
|
||||
actions.addTransform({
|
||||
mediaType: transform.MIMETYPE,
|
||||
component: transform,
|
||||
})
|
||||
);
|
||||
},
|
||||
runCell: (contentRef: ContentRef, cellId: string) => {
|
||||
return dispatch(
|
||||
actions.executeCell({
|
||||
contentRef,
|
||||
id: cellId,
|
||||
})
|
||||
);
|
||||
},
|
||||
updateCell: (text: string, id: string, contentRef: ContentRef) => {
|
||||
dispatch(actions.updateCellSource({ id, contentRef, value: text }));
|
||||
},
|
||||
};
|
||||
};
|
||||
return mapDispatchToProps;
|
||||
};
|
||||
|
||||
export default connect(makeMapStateToProps, makeMapDispatchToProps)(DataUploader);
|
||||
48
src/Explorer/Notebook/DataUploader/DataUploaderAdapter.tsx
Normal file
48
src/Explorer/Notebook/DataUploader/DataUploaderAdapter.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { actions, createContentRef, createKernelRef, KernelRef } from "@nteract/core";
|
||||
import DataUploader from "Explorer/Notebook/DataUploader/DataUploader";
|
||||
import * as React from "react";
|
||||
import { Provider } from "react-redux";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import {
|
||||
NotebookComponentBootstrapper,
|
||||
NotebookComponentBootstrapperOptions,
|
||||
} from "../NotebookComponent/NotebookComponentBootstrapper";
|
||||
import { DataUploaderNotebook } from "./DataUploaderUtils";
|
||||
|
||||
export class DataUploaderAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
|
||||
public parameters: unknown;
|
||||
private kernelRef: KernelRef;
|
||||
|
||||
constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) {
|
||||
super(options);
|
||||
|
||||
if (!this.contentRef) {
|
||||
this.contentRef = createContentRef();
|
||||
this.kernelRef = createKernelRef();
|
||||
|
||||
this.getStore().dispatch(
|
||||
actions.fetchContent({
|
||||
filepath: DataUploaderNotebook.path,
|
||||
params: {},
|
||||
kernelRef: this.kernelRef,
|
||||
contentRef: this.contentRef,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
const props = {
|
||||
contentRef: this.contentRef,
|
||||
kernelRef: this.kernelRef,
|
||||
databaseId: this.databaseId,
|
||||
collectionId: this.collectionId,
|
||||
};
|
||||
|
||||
return (
|
||||
<Provider store={this.getStore()}>
|
||||
<DataUploader {...props} />;
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
44
src/Explorer/Notebook/DataUploader/DataUploaderUtils.ts
Normal file
44
src/Explorer/Notebook/DataUploader/DataUploaderUtils.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Notebook } from "@nteract/commutable";
|
||||
import { IContent } from "@nteract/types";
|
||||
import * as InMemoryContentProviderUtils from "../NotebookComponent/ContentProviders/InMemoryContentProviderUtils";
|
||||
|
||||
const notebookName = "data-uploader-component-notebook.ipynb";
|
||||
const notebookPath = InMemoryContentProviderUtils.toContentUri(notebookName);
|
||||
const notebook: Notebook = {
|
||||
cells: [
|
||||
{
|
||||
cell_type: "code",
|
||||
metadata: {},
|
||||
execution_count: 0,
|
||||
outputs: [],
|
||||
source: "",
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
kernelspec: {
|
||||
displayName: "Mongo",
|
||||
language: "mongocli",
|
||||
name: "mongo",
|
||||
},
|
||||
language_info: {
|
||||
file_extension: "ipynb",
|
||||
mimetype: "application/json",
|
||||
name: "mongo",
|
||||
version: "1.0",
|
||||
},
|
||||
},
|
||||
nbformat: 4,
|
||||
nbformat_minor: 4,
|
||||
};
|
||||
|
||||
export const DataUploaderNotebook: IContent<"notebook"> = {
|
||||
name: notebookName,
|
||||
path: notebookPath,
|
||||
type: "notebook",
|
||||
writable: true,
|
||||
created: "",
|
||||
last_modified: "",
|
||||
mimetype: "application/x-ipynb+json",
|
||||
content: notebook,
|
||||
format: "json",
|
||||
};
|
||||
@@ -19,6 +19,7 @@ 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();
|
||||
@@ -27,34 +28,34 @@ export class NotebookContainerClient {
|
||||
maxTimeout: Notebook.retryAttemptDelayMs,
|
||||
minTimeout: Notebook.retryAttemptDelayMs,
|
||||
};
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo?.notebookServerEndpoint) {
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
this.initHeartbeat(Constants.Notebook.heartbeatDelayMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { ImmutableNotebook } from "@nteract/commutable";
|
||||
import type { IContentProvider } from "@nteract/core";
|
||||
import { DataUploaderNotebook } from "Explorer/Notebook/DataUploader/DataUploaderUtils";
|
||||
import React from "react";
|
||||
import { contents } from "rx-jupyter";
|
||||
import { Areas, HttpStatusCodes } from "../../Common/Constants";
|
||||
@@ -68,6 +69,10 @@ export default class NotebookManager {
|
||||
readonly: true,
|
||||
content: SchemaAnalyzerNotebook,
|
||||
},
|
||||
[DataUploaderNotebook.path]: {
|
||||
readonly: true,
|
||||
content: DataUploaderNotebook,
|
||||
},
|
||||
});
|
||||
|
||||
this.gitHubContentProvider = new GitHubContentProvider({
|
||||
|
||||
@@ -26,6 +26,7 @@ describe("OpenActions", () => {
|
||||
collection.onDocumentDBDocumentsClick = jest.fn();
|
||||
collection.onMongoDBDocumentsClick = jest.fn();
|
||||
collection.onSchemaAnalyzerClick = jest.fn();
|
||||
collection.onDataUploaderClick = jest.fn();
|
||||
collection.onTableEntitiesClick = jest.fn();
|
||||
collection.onGraphDocumentsClick = jest.fn();
|
||||
collection.onNewQueryClick = jest.fn();
|
||||
|
||||
@@ -86,6 +86,14 @@ function openCollectionTab(
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.DataUploader ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.DataUploader]
|
||||
) {
|
||||
collection.onDataUploaderClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.TableEntities ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
|
||||
|
||||
@@ -249,6 +249,7 @@ 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) =>
|
||||
@@ -483,6 +484,7 @@ 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) =>
|
||||
|
||||
@@ -146,9 +146,10 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
// TODO add feature flag that disables validation for customers with custom accounts
|
||||
if (isAutoscaleSelected) {
|
||||
if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) {
|
||||
setFormErrors(
|
||||
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
|
||||
);
|
||||
const minAutoPilotThroughput = userContext.features.freetierAutoscaleThroughput
|
||||
? AutoPilotUtils.autoPilotThroughput1K
|
||||
: AutoPilotUtils.autoPilotThroughput4K;
|
||||
setFormErrors(`Please enter a value greater than ${minAutoPilotThroughput} for autopilot throughput`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -241,6 +242,7 @@ 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,6 +262,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
}
|
||||
isDatabase
|
||||
isSharded
|
||||
isFreeTier={isFreeTierAccount}
|
||||
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
|
||||
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
|
||||
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
|
||||
@@ -335,6 +336,7 @@ 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,7 +10,6 @@ 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";
|
||||
|
||||
@@ -104,11 +103,14 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
switch (location.type) {
|
||||
case "MyNotebooks":
|
||||
parent = {
|
||||
name: ResourceTreeAdapter.MyNotebooksTitle,
|
||||
name: useNotebook.getState().notebookFolderName,
|
||||
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
@@ -1,50 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,121 +0,0 @@
|
||||
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,6 +35,60 @@ 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 },
|
||||
@@ -54,6 +108,60 @@ 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,6 +27,60 @@ 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 = {
|
||||
|
||||
41
src/Explorer/Tabs/DataUploaderTab.ts
Normal file
41
src/Explorer/Tabs/DataUploaderTab.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { DataUploaderAdapter } from "../Notebook/DataUploader/DataUploaderAdapter";
|
||||
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
|
||||
|
||||
export default class DataUploaderTab extends NotebookTabBase {
|
||||
public readonly html = '<div data-bind="react:DataUploaderAdapter" style="height: 100%"></div>';
|
||||
private DataUploaderAdapter: DataUploaderAdapter;
|
||||
|
||||
constructor(options: NotebookTabBaseOptions) {
|
||||
super(options);
|
||||
this.DataUploaderAdapter = new DataUploaderAdapter(
|
||||
{
|
||||
contentRef: undefined,
|
||||
notebookClient: NotebookTabBase.clientManager,
|
||||
},
|
||||
options.collection?.databaseId,
|
||||
options.collection?.id()
|
||||
);
|
||||
}
|
||||
|
||||
public onActivate(): void {
|
||||
traceSuccess(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseName: this.collection?.databaseId,
|
||||
collectionName: this.collection?.id,
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: "Upload",
|
||||
},
|
||||
this.onLoadStartKey
|
||||
);
|
||||
|
||||
super.onActivate();
|
||||
}
|
||||
|
||||
protected buildCommandBarOptions(): void {
|
||||
this.updateNavbarWithTabsButtons();
|
||||
}
|
||||
}
|
||||
@@ -215,13 +215,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
{
|
||||
metric: "Request Charge",
|
||||
value: this.state.requestChargeDisplayText,
|
||||
toolTip: "",
|
||||
toolTip: "Request Charge",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
{
|
||||
metric: "Showing Results",
|
||||
value: this.state.showingDocumentsDisplayText,
|
||||
toolTip: "",
|
||||
toolTip: "Showing Results",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -574,6 +574,52 @@ export default class Collection implements ViewModels.Collection {
|
||||
);
|
||||
};
|
||||
|
||||
public onDataUploaderClick = async () => {
|
||||
if (useNotebook.getState().isPhoenixFeatures) {
|
||||
await this.container.allocateContainer();
|
||||
}
|
||||
useSelectedNode.getState().setSelectedNode(this);
|
||||
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
|
||||
const DataUploaderTab = await (await import("../Tabs/DataUploaderTab")).default;
|
||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||
description: "Data uploader node",
|
||||
databaseName: this.databaseId,
|
||||
collectionName: this.id(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
});
|
||||
|
||||
for (const tab of useTabs.getState().openedTabs) {
|
||||
if (
|
||||
tab instanceof DataUploaderTab &&
|
||||
tab.collection?.databaseId === this.databaseId &&
|
||||
tab.collection?.id() === this.id()
|
||||
) {
|
||||
return useTabs.getState().activateTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
const startKey = TelemetryProcessor.traceStart(Action.Tab, {
|
||||
databaseName: this.databaseId,
|
||||
collectionName: this.id(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: "Upload",
|
||||
});
|
||||
this.documentIds([]);
|
||||
useTabs.getState().activateNewTab(
|
||||
new DataUploaderTab({
|
||||
account: userContext.databaseAccount,
|
||||
masterKey: userContext.masterKey || "",
|
||||
container: this.container,
|
||||
tabKind: ViewModels.CollectionTabKind.DataUploader,
|
||||
title: "Upload",
|
||||
tabPath: "",
|
||||
collection: this,
|
||||
node: this,
|
||||
onLoadStartKey: startKey,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
public onSettingsClick = async (): Promise<void> => {
|
||||
useSelectedNode.getState().setSelectedNode(this);
|
||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||
|
||||
@@ -526,6 +526,15 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
.getState()
|
||||
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]),
|
||||
});
|
||||
|
||||
children.push({
|
||||
label: "Data Upload (Preview)",
|
||||
onClick: collection.onDataUploaderClick.bind(collection),
|
||||
isSelected: () =>
|
||||
useSelectedNode
|
||||
.getState()
|
||||
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.DataUploader]),
|
||||
});
|
||||
}
|
||||
|
||||
if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) {
|
||||
|
||||
@@ -283,6 +283,15 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
.getState()
|
||||
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]),
|
||||
});
|
||||
|
||||
children.push({
|
||||
label: "Data Upload (Preview)",
|
||||
onClick: collection.onDataUploaderClick.bind(collection),
|
||||
isSelected: () =>
|
||||
useSelectedNode
|
||||
.getState()
|
||||
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.DataUploader]),
|
||||
});
|
||||
}
|
||||
|
||||
if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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";
|
||||
@@ -484,7 +485,7 @@ export class JunoClient {
|
||||
// public for tests
|
||||
public static getJunoEndpoint(): string {
|
||||
const junoEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
||||
if (configContext.allowedJunoOrigins.indexOf(new URL(junoEndpoint).origin) === -1) {
|
||||
if (!validateEndpoint(junoEndpoint, allowedJunoOrigins)) {
|
||||
const error = `${junoEndpoint} not allowed as juno endpoint`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
|
||||
import {
|
||||
Areas,
|
||||
ConnectionStatusType,
|
||||
@@ -154,7 +155,7 @@ export class PhoenixClient {
|
||||
public static getPhoenixEndpoint(): string {
|
||||
const phoenixEndpoint =
|
||||
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
||||
if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) {
|
||||
if (!validateEndpoint(phoenixEndpoint, allowedJunoOrigins)) {
|
||||
const error = `${phoenixEndpoint} not allowed as juno endpoint`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export type Features = {
|
||||
// set only via feature flags
|
||||
readonly canExceedMaximumValue: boolean;
|
||||
readonly cosmosdb: boolean;
|
||||
readonly enableChangeFeedPolicy: boolean;
|
||||
@@ -8,12 +9,6 @@ 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;
|
||||
@@ -23,7 +18,6 @@ export type Features = {
|
||||
readonly hostedDataExplorer: boolean;
|
||||
readonly junoEndpoint?: string;
|
||||
readonly phoenixEndpoint?: string;
|
||||
readonly livyEndpoint?: string;
|
||||
readonly notebookBasePath?: string;
|
||||
readonly notebookServerToken?: string;
|
||||
readonly notebookServerUrl?: string;
|
||||
@@ -35,11 +29,22 @@ 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;
|
||||
freetierAutoscaleThroughput: 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;
|
||||
|
||||
@@ -72,7 +77,6 @@ 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"),
|
||||
@@ -84,10 +88,9 @@ 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"),
|
||||
freetierAutoscaleThroughput: "true" === get("freetierautoscalethroughput"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
export const minAutoPilotThroughput = 4000;
|
||||
import { userContext } from "UserContext";
|
||||
|
||||
export const autoPilotThroughput1K = 1000;
|
||||
export const autoPilotIncrementStep = 1000;
|
||||
export const autoPilotThroughput4K = 4000;
|
||||
|
||||
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
||||
if (!maxThroughput) {
|
||||
return false;
|
||||
}
|
||||
const minAutoPilotThroughput = userContext.features.freetierAutoscaleThroughput
|
||||
? autoPilotThroughput4K
|
||||
: autoPilotThroughput1K;
|
||||
if (maxThroughput < minAutoPilotThroughput) {
|
||||
return false;
|
||||
}
|
||||
|
||||
87
src/Utils/EndpointValidation.ts
Normal file
87
src/Utils/EndpointValidation.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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> = [];
|
||||
@@ -4,7 +4,7 @@ export function isInvalidParentFrameOrigin(event: MessageEvent): boolean {
|
||||
return !isValidOrigin(configContext.allowedParentFrameOrigins, event);
|
||||
}
|
||||
|
||||
function isValidOrigin(allowedOrigins: string[], event: MessageEvent): boolean {
|
||||
function isValidOrigin(allowedOrigins: ReadonlyArray<string>, event: MessageEvent): boolean {
|
||||
const eventOrigin = (event && event.origin) || "";
|
||||
const windowOrigin = (window && window.origin) || "";
|
||||
if (eventOrigin === windowOrigin) {
|
||||
|
||||
@@ -351,6 +351,11 @@ 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;
|
||||
@@ -373,6 +378,9 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
if (inputs.flights.indexOf(Flights.NotebooksDownBanner) !== -1) {
|
||||
userContext.features.notebooksDownBanner = true;
|
||||
}
|
||||
if (inputs.flights.indexOf(Flights.FreeTierAutoscaleThroughput) !== -1) {
|
||||
userContext.features.freetierAutoscaleThroughput = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user