Compare commits

..

1 Commits

Author SHA1 Message Date
Laurent Nguyen
4cc98f0b62 Initial implementation of dropzone component 2022-03-16 17:42:42 +01:00
31 changed files with 1090 additions and 31578 deletions

31963
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -52,10 +52,10 @@ 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$`,
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`,
], // Webpack injects this at build time
gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/",

View File

@@ -9,6 +9,7 @@ export enum TabKind {
Graph,
SQLQuery,
ScaleSettings,
DataUploader,
}
/**

View File

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

View File

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

View File

@@ -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[] => {

View File

@@ -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 { 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={autoPilotThroughput1K}
min={userContext.features.freetierAutoscaleThroughput ? autoPilotThroughput1K : autoPilotThroughput4K}
errorMessage={this.props.throughputError}
/>
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}

View File

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

View File

@@ -34,7 +34,9 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
}: ThroughputInputProps) => {
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
const [throughput, setThroughput] = useState<number>(
isFreeTier ? AutoPilotUtils.autoPilotThroughput1K : AutoPilotUtils.autoPilotThroughput4K
isFreeTier && userContext.features.freetierAutoscaleThroughput
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K
);
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
const [throughputError, setThroughputError] = useState<string>("");
@@ -155,9 +157,10 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
if (mode === "Autoscale") {
const defaultThroughput = isFreeTier
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K;
const defaultThroughput =
isFreeTier && userContext.features.freetierAutoscaleThroughput
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K;
setThroughput(defaultThroughput);
setIsAutoScaleSelected(true);
setThroughputValue(defaultThroughput);
@@ -233,7 +236,11 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
}}
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.autoPilotThroughput1K}
min={
userContext.features.freetierAutoscaleThroughput
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K
}
value={throughput.toString()}
aria-label="Max request units per second"
required={true}

View File

@@ -1638,7 +1638,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 +1660,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 +1956,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]}

View File

@@ -362,13 +362,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 +386,12 @@ 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."
);
}
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);

View File

@@ -0,0 +1,5 @@
.dataUploader {
width: 100%;
height: 100%;
overflow-y: auto;
}

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

View 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>
);
}
}

View 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",
};

View File

@@ -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";
@@ -160,16 +159,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;
}
}

View File

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

View File

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

View File

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

View File

@@ -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.autoPilotThroughput1K} 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;
}
}

View 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();
}
}

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import { useDialog } from "Explorer/Controls/Dialog";
import promiseRetry, { AbortError } from "p-retry";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
@@ -17,14 +16,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 +45,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 +108,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);
@@ -197,48 +177,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;
}
}
}
}

View File

@@ -37,6 +37,7 @@ export type Features = {
phoenixNotebooks?: boolean;
phoenixFeatures?: boolean;
notebooksDownBanner: boolean;
freetierAutoscaleThroughput: boolean;
};
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features {
@@ -89,6 +90,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
notebooksDownBanner: "true" === get("notebooksDownBanner"),
enableThroughputCap: "true" === get("enablethroughputcap"),
freetierAutoscaleThroughput: "true" === get("freetierautoscalethroughput"),
};
}

View File

@@ -1,3 +1,5 @@
import { userContext } from "UserContext";
export const autoPilotThroughput1K = 1000;
export const autoPilotIncrementStep = 1000;
export const autoPilotThroughput4K = 4000;
@@ -6,7 +8,10 @@ export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
if (!maxThroughput) {
return false;
}
if (maxThroughput < autoPilotThroughput1K) {
const minAutoPilotThroughput = userContext.features.freetierAutoscaleThroughput
? autoPilotThroughput4K
: autoPilotThroughput1K;
if (maxThroughput < minAutoPilotThroughput) {
return false;
}
if (maxThroughput % 1000) {

View File

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

View File

@@ -378,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;
}
}
}