Compare commits

..

1 Commits

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

31969
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

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

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

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

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

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

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