mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-22 02:11:29 +00:00
Merge branch 'master' of https://github.com/Azure/cosmos-explorer into genericRightPaneComponent
This commit is contained in:
@@ -1,4 +1,13 @@
|
||||
import { ChoiceGroup, IChoiceGroupOption, Label, MessageBar, Stack, Text, TextField } from "office-ui-fabric-react";
|
||||
import {
|
||||
ChoiceGroup,
|
||||
IChoiceGroupOption,
|
||||
Label,
|
||||
Link,
|
||||
MessageBar,
|
||||
Stack,
|
||||
Text,
|
||||
TextField,
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import { userContext } from "../../../../UserContext";
|
||||
@@ -61,17 +70,15 @@ export interface SubSettingsComponentProps {
|
||||
|
||||
export class SubSettingsComponent extends React.Component<SubSettingsComponentProps> {
|
||||
private shouldCheckComponentIsDirty = true;
|
||||
private ttlVisible: boolean;
|
||||
private geospatialVisible: boolean;
|
||||
private partitionKeyValue: string;
|
||||
private partitionKeyName: string;
|
||||
|
||||
constructor(props: SubSettingsComponentProps) {
|
||||
super(props);
|
||||
this.ttlVisible = (this.props.container && !this.props.container.isPreferredApiCassandra()) || false;
|
||||
this.geospatialVisible = userContext.apiType === "SQL";
|
||||
this.partitionKeyValue = "/" + this.props.collection.partitionKeyProperty;
|
||||
this.partitionKeyName = this.props.container.isPreferredApiMongoDB() ? "Shard key" : "Partition key";
|
||||
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -171,39 +178,51 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
): void =>
|
||||
this.props.onChangeFeedPolicyChange(ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState]);
|
||||
|
||||
private getTtlComponent = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<ChoiceGroup
|
||||
id="timeToLive"
|
||||
label="Time to Live"
|
||||
selectedKey={this.props.timeToLive}
|
||||
options={this.ttlChoiceGroupOptions}
|
||||
onChange={this.onTtlChange}
|
||||
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
|
||||
/>
|
||||
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||
styles={messageBarStyles}
|
||||
>
|
||||
{ttlWarning}
|
||||
</MessageBar>
|
||||
)}
|
||||
{this.props.timeToLive === TtlType.On && (
|
||||
<TextField
|
||||
id="timeToLiveSeconds"
|
||||
styles={getTextFieldStyles(this.props.timeToLiveSeconds, this.props.timeToLiveSecondsBaseline)}
|
||||
type="number"
|
||||
required
|
||||
min={1}
|
||||
max={Int32.Max}
|
||||
value={this.props.timeToLiveSeconds?.toString()}
|
||||
onChange={this.onTimeToLiveSecondsChange}
|
||||
suffix="second(s)"
|
||||
private getTtlComponent = (): JSX.Element =>
|
||||
userContext.apiType === "Mongo" ? (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||
styles={{ text: { fontSize: 14 } }}
|
||||
>
|
||||
To enable time-to-live (TTL) for your collection/documents,
|
||||
<Link href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live" target="_blank">
|
||||
create a TTL index
|
||||
</Link>
|
||||
.
|
||||
</MessageBar>
|
||||
) : (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<ChoiceGroup
|
||||
id="timeToLive"
|
||||
label="Time to Live"
|
||||
selectedKey={this.props.timeToLive}
|
||||
options={this.ttlChoiceGroupOptions}
|
||||
onChange={this.onTtlChange}
|
||||
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||
styles={messageBarStyles}
|
||||
>
|
||||
{ttlWarning}
|
||||
</MessageBar>
|
||||
)}
|
||||
{this.props.timeToLive === TtlType.On && (
|
||||
<TextField
|
||||
id="timeToLiveSeconds"
|
||||
styles={getTextFieldStyles(this.props.timeToLiveSeconds, this.props.timeToLiveSecondsBaseline)}
|
||||
type="number"
|
||||
required
|
||||
min={1}
|
||||
max={Int32.Max}
|
||||
value={this.props.timeToLiveSeconds?.toString()}
|
||||
onChange={this.onTimeToLiveSecondsChange}
|
||||
suffix="second(s)"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: TtlType.Off, text: "Off", disabled: true },
|
||||
@@ -316,7 +335,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack {...subComponentStackProps}>
|
||||
{this.ttlVisible && this.getTtlComponent()}
|
||||
{userContext.apiType !== "Cassandra" && this.getTtlComponent()}
|
||||
|
||||
{this.geospatialVisible && this.getGeoSpatialComponent()}
|
||||
|
||||
|
||||
@@ -1149,7 +1149,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
isAddRemoveVisible={false}
|
||||
onParamKeyChange={[Function]}
|
||||
onParamValueChange={[Function]}
|
||||
paramValue=""
|
||||
selectedKey="string"
|
||||
>
|
||||
<StyledLabelBase>
|
||||
@@ -2684,7 +2683,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
key=".0:$.1"
|
||||
label="Value"
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
>
|
||||
<TextFieldBase
|
||||
autoFocus={true}
|
||||
@@ -2969,7 +2967,6 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
|
||||
}
|
||||
}
|
||||
validateOnLoad={true}
|
||||
value=""
|
||||
>
|
||||
<div
|
||||
className="ms-TextField root-70"
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ExecuteSprocParamsPanel: FunctionComponent<ExecuteSprocParamsPanePr
|
||||
}: ExecuteSprocParamsPaneProps): JSX.Element => {
|
||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
||||
const [paramKeyValues, setParamKeyValues] = useState<UnwrappedExecuteSprocParam[]>([{ key: "string", text: "" }]);
|
||||
const [partitionValue, setPartitionValue] = useState<string>("");
|
||||
const [partitionValue, setPartitionValue] = useState<string>(); // Defaulting to undefined here is important. It is not the same partition key as ""
|
||||
const [selectedKey, setSelectedKey] = React.useState<IDropdownOption>({ key: "string", text: "" });
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { DetailsList, DetailsListLayoutMode, IColumn, SelectionMode } from "office-ui-fabric-react";
|
||||
import React, { ChangeEvent, FunctionComponent, useState } from "react";
|
||||
import { Upload } from "../../../Common/Upload";
|
||||
import { UploadDetailsRecord } from "../../../Contracts/ViewModels";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { UploadDetails, UploadDetailsRecord } from "../../../workers/upload/definitions";
|
||||
import Explorer from "../../Explorer";
|
||||
import { getErrorMessage } from "../../Tables/Utilities";
|
||||
import { RightPaneWrapper, RightPaneWrapperProps } from "../RightPaneWrapper/RightPaneWrapper";
|
||||
@@ -13,12 +13,6 @@ export interface UploadItemsPaneProps {
|
||||
closePanel: () => void;
|
||||
}
|
||||
|
||||
interface IUploadFileData {
|
||||
numSucceeded: number;
|
||||
numFailed: number;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
const getTitle = (): string => {
|
||||
if (userContext.apiType === "Cassandra" || userContext.apiType === "Tables") {
|
||||
return "Upload Tables";
|
||||
@@ -54,7 +48,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
|
||||
selectedCollection
|
||||
?.uploadFiles(files)
|
||||
.then(
|
||||
(uploadDetails: UploadDetails) => {
|
||||
(uploadDetails) => {
|
||||
setUploadFileData(uploadDetails.data);
|
||||
setFiles(undefined);
|
||||
},
|
||||
@@ -84,6 +78,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
|
||||
onClose: closePanel,
|
||||
onSubmit,
|
||||
};
|
||||
|
||||
const columns: IColumn[] = [
|
||||
{
|
||||
key: "fileName",
|
||||
@@ -105,12 +100,12 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({
|
||||
},
|
||||
];
|
||||
|
||||
const _renderItemColumn = (item: IUploadFileData, index: number, column: IColumn) => {
|
||||
const _renderItemColumn = (item: UploadDetailsRecord, index: number, column: IColumn) => {
|
||||
switch (column.key) {
|
||||
case "status":
|
||||
return <span>{item.numSucceeded + " items created, " + item.numFailed + " errors"}</span>;
|
||||
return `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
|
||||
default:
|
||||
return <span>{item.fileName}</span>;
|
||||
return item.fileName;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import * as ko from "knockout";
|
||||
import * as _ from "underscore";
|
||||
import UploadWorker from "worker-loader!../../workers/upload";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { bulkCreateDocument } from "../../Common/dataAccess/bulkCreateDocument";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
|
||||
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
|
||||
@@ -13,16 +12,13 @@ import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefine
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { UploadDetailsRecord } from "../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions";
|
||||
import Explorer from "../Explorer";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
|
||||
import ConflictsTab from "../Tabs/ConflictsTab";
|
||||
import DocumentsTab from "../Tabs/DocumentsTab";
|
||||
@@ -958,73 +954,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
this.uploadFiles(event.originalEvent.dataTransfer.files);
|
||||
}
|
||||
|
||||
public uploadFiles = (fileList: FileList): Promise<UploadDetails> => {
|
||||
// TODO: right now web worker is not working with AAD flow. Use main thread for upload for now until we have backend upload capability
|
||||
if (configContext.platform === Platform.Hosted && userContext.authType === AuthType.AAD) {
|
||||
return this._uploadFilesCors(fileList);
|
||||
}
|
||||
const documentUploader: Worker = new UploadWorker();
|
||||
let inProgressNotificationId: string = "";
|
||||
|
||||
if (!fileList || fileList.length === 0) {
|
||||
return Promise.reject("No files specified");
|
||||
}
|
||||
|
||||
const onmessage = (resolve: (value: UploadDetails) => void, reject: (reason: any) => void, event: MessageEvent) => {
|
||||
const numSuccessful: number = event.data.numUploadsSuccessful;
|
||||
const numFailed: number = event.data.numUploadsFailed;
|
||||
const runtimeError: string = event.data.runtimeError;
|
||||
const uploadDetails: UploadDetails = event.data.uploadDetails;
|
||||
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressNotificationId);
|
||||
documentUploader.terminate();
|
||||
if (!!runtimeError) {
|
||||
reject(runtimeError);
|
||||
} else if (numSuccessful === 0) {
|
||||
// all uploads failed
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to upload all documents to container ${this.id()}`);
|
||||
} else if (numFailed > 0) {
|
||||
NotificationConsoleUtils.logConsoleError(
|
||||
`Failed to upload ${numFailed} of ${numSuccessful + numFailed} documents to container ${this.id()}`
|
||||
);
|
||||
} else {
|
||||
NotificationConsoleUtils.logConsoleInfo(
|
||||
`Successfully uploaded all ${numSuccessful} documents to container ${this.id()}`
|
||||
);
|
||||
}
|
||||
this._logUploadDetailsInConsole(uploadDetails);
|
||||
resolve(uploadDetails);
|
||||
};
|
||||
function onerror(reject: (reason: any) => void, event: ErrorEvent) {
|
||||
documentUploader.terminate();
|
||||
reject(event.error);
|
||||
}
|
||||
|
||||
const uploaderMessage: StartUploadMessageParams = {
|
||||
files: fileList,
|
||||
documentClientParams: {
|
||||
databaseId: this.databaseId,
|
||||
containerId: this.id(),
|
||||
masterKey: userContext.masterKey,
|
||||
endpoint: userContext.endpoint,
|
||||
accessToken: userContext.accessToken,
|
||||
platform: configContext.platform,
|
||||
databaseAccount: userContext.databaseAccount,
|
||||
},
|
||||
};
|
||||
|
||||
return new Promise<UploadDetails>((resolve, reject) => {
|
||||
documentUploader.onmessage = onmessage.bind(null, resolve, reject);
|
||||
documentUploader.onerror = onerror.bind(null, reject);
|
||||
|
||||
documentUploader.postMessage(uploaderMessage);
|
||||
inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Uploading and creating documents in container ${this.id()}`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
public async getPendingThroughputSplitNotification(): Promise<DataModels.Notification> {
|
||||
if (!this.container) {
|
||||
return undefined;
|
||||
@@ -1060,13 +989,13 @@ export default class Collection implements ViewModels.Collection {
|
||||
}
|
||||
}
|
||||
|
||||
private async _uploadFilesCors(files: FileList): Promise<UploadDetails> {
|
||||
const data = await Promise.all(Array.from(files).map((file) => this._uploadFile(file)));
|
||||
public async uploadFiles(files: FileList): Promise<{ data: UploadDetailsRecord[] }> {
|
||||
const data = await Promise.all(Array.from(files).map((file) => this.uploadFile(file)));
|
||||
|
||||
return { data };
|
||||
}
|
||||
|
||||
private _uploadFile(file: File): Promise<UploadDetailsRecord> {
|
||||
private uploadFile(file: File): Promise<UploadDetailsRecord> {
|
||||
const reader = new FileReader();
|
||||
const onload = (resolve: (value: UploadDetailsRecord) => void, evt: any): void => {
|
||||
const fileData: string = evt.target.result;
|
||||
@@ -1077,6 +1006,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
resolve({
|
||||
fileName: file.name,
|
||||
numSucceeded: 0,
|
||||
numThrottled: 0,
|
||||
numFailed: 1,
|
||||
errors: [(evt as any).error.message],
|
||||
});
|
||||
@@ -1094,21 +1024,32 @@ export default class Collection implements ViewModels.Collection {
|
||||
fileName: fileName,
|
||||
numSucceeded: 0,
|
||||
numFailed: 0,
|
||||
numThrottled: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const content = JSON.parse(documentContent);
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
await Promise.all(
|
||||
content.map(async (documentContent) => {
|
||||
await createDocument(this, documentContent);
|
||||
record.numSucceeded++;
|
||||
})
|
||||
const parsedContent = JSON.parse(documentContent);
|
||||
if (Array.isArray(parsedContent)) {
|
||||
const chunkSize = 50; // 100 is the max # of bulk operations the SDK currently accepts but usually results in throttles on 400RU collections
|
||||
const chunkedContent = Array.from({ length: Math.ceil(parsedContent.length / chunkSize) }, (_, index) =>
|
||||
parsedContent.slice(index * chunkSize, index * chunkSize + chunkSize)
|
||||
);
|
||||
for (const chunk of chunkedContent) {
|
||||
const responses = await bulkCreateDocument(this, chunk);
|
||||
for (const response of responses) {
|
||||
if (response.statusCode === 201) {
|
||||
record.numSucceeded++;
|
||||
} else if (response.statusCode === 429) {
|
||||
record.numThrottled++;
|
||||
} else {
|
||||
record.numFailed++;
|
||||
record.errors = [...record.errors, `${response.statusCode} ${response.resourceBody}`];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await createDocument(this, documentContent);
|
||||
await createDocument(this, parsedContent);
|
||||
record.numSucceeded++;
|
||||
}
|
||||
|
||||
@@ -1120,40 +1061,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
}
|
||||
}
|
||||
|
||||
private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void {
|
||||
const uploadDetailsRecords: UploadDetailsRecord[] = uploadDetails.data;
|
||||
const numFiles: number = uploadDetailsRecords.length;
|
||||
const stackTraceLimit: number = 100;
|
||||
let stackTraceCount: number = 0;
|
||||
let currentFileIndex = 0;
|
||||
while (stackTraceCount < stackTraceLimit && currentFileIndex < numFiles) {
|
||||
const errors: string[] = uploadDetailsRecords[currentFileIndex].errors;
|
||||
for (let i = 0; i < errors.length; i++) {
|
||||
if (stackTraceCount >= stackTraceLimit) {
|
||||
break;
|
||||
}
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Document creation error for container ${this.id()} - file ${
|
||||
uploadDetailsRecords[currentFileIndex].fileName
|
||||
}: ${errors[i]}`
|
||||
);
|
||||
stackTraceCount++;
|
||||
}
|
||||
currentFileIndex++;
|
||||
}
|
||||
|
||||
uploadDetailsRecords.forEach((record: UploadDetailsRecord) => {
|
||||
const consoleDataType: ConsoleDataType = record.numFailed > 0 ? ConsoleDataType.Error : ConsoleDataType.Info;
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
consoleDataType,
|
||||
`Item creation summary for container ${this.id()} - file ${record.fileName}: ${
|
||||
record.numSucceeded
|
||||
} items created, ${record.numFailed} errors`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level method that will open the correct tab type depending on account API
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user