mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 02:41:39 +00:00
Compare commits
2 Commits
users/lang
...
users/lang
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8ffcc4591 | ||
|
|
e220e0e74b |
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
<svg width="15" height="15" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" overflow="hidden"><defs><clipPath id="clip0"><rect x="479" y="279" width="15" height="15"/></clipPath><clipPath id="clip1"><rect x="-0.287396" y="-0.171573" width="152381" height="152381"/></clipPath><image width="35" height="35" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAMAAAApB0NrAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAH4UExURQAAAASExCGs7CC//wB9vQB5uxSU1yaz8ySy9AB8uwB8vAB5uxOR1Say8yax8iaw8gB6vAB6uwB7uwB4uROQ1Cax8ySy8QCAvwB3ugB5ugB2uROP1CWw8yav8wCT2QCQ1QCP1wB2uQB1uBOO0yWv8yat8wCP1QCP1QCP1QCO1QCQ1wB1tQB2uAB3uAB1tx+k6SSt8ySt8QCN0wCO1ACM0wBzuQBztAB1twB0tgB0tiSr8SSs8gCM1ACM1ACEywBwtQBxtAB0tgBztQB0sySp8SSr8gCL0wCK0gCL0wCHzwByuABusgBztQBttiOq8gB6wQCI0QCJ0gB7wySn8SOo8gBxtABtsABwtgCFzwCI0gCG0QCJ0SKn8SKn8iKl8QBvsABvsgBvsQBtsABssgCCzACG0QCF0ACDzyKm8QBtrwBusABsrgCAzQCE0ACF0ACF0ACE0CKj8SKl8QBssQBtrwBtrwBsrgBsrwCK1QCBzwCD0ACA0B2d7CGj8QBsrABrrQBwrwCA3wCC0ACCzwCBzwB+zhGM3iGi8SKh8QCAzQCAzgCAzwB9zRCK3iCh8SGh8SCf8QB+zAB/zgB8zRCJ3SCg8SCf7wB+zQB6zBCI3CCe8CCf8CCe8CCf8SCf/wB7zAB4yxGJ3h6c8CCd7yCf7wmA0RqX60C//5CaUeMAAACodFJOUwA8XAho+//ncID/////53iM//////9wCKv/////gCiYILf///+AMPP/70wYw/+3lP+AXP+MKNv/+3uA/3z/+////+NAgP9c9/////+7HP+A///MgP9Y+////7scgP+AeP/////7/+NA/2D/iyTb//t4gP808//vUBjD/7eU/yifIAi3//////+Ar////////4CI/////3B//////+9/CGj7/+dwEDhYBCm1XqwAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAFpSURBVDhPY2AYBaiAkYkZXQgdsLCysXPgV8XJxc3Dy8vHj0eVgKCQsIioqKioGLoMDIhLSEpKSknLyMjIyKLLyckrgJUoCgsLCyspq6ioqKiiKVFT19DUYmDQ1tEFAT19AwMDA0M0NUbGxsbGJqZm5ubm5haWDFbW1tbWVmhqGGxsbW1t7ewdHB2dnBkYXFxdXV1d0NUwuLl7eHh4enn7+DIwMLj4+fn5Yaph8A8IDAwMDBIHsYNDQkJCgtFVMISGhUdERkZGRkUzMDDExMbGxsahK4lPSExKTklNTU1NS2dgiMvIyMhAV5OZlZWVlZ2Tm5eXl5dfwFBYVFRUVIimpriktKycgaGisgoEqmtqa2tr0dUw1NU3gKjGpuaWlpbWtvb29vYOdDUw0NjZ1dXd09vX39c3AV0OASZOmjR5ytSpU6dOQ5dBAtN7ZsycNXvO3HnoEshg/oKFixYvQRdFA0uXLUcXGqEAAH4FV0z+qQbjAAAAAElFTkSuQmCC" preserveAspectRatio="none" id="img2"></image><clipPath id="clip3"><path d="M44291.4 46947.4 187148 46947.4 187148 188823 44291.4 188823Z" fill-rule="evenodd" clip-rule="evenodd"/></clipPath></defs><g clip-path="url(#clip0)" transform="translate(-479 -279)"><g clip-path="url(#clip1)" transform="matrix(0.000105 0 0 0.000105 479 279)"><g clip-path="url(#clip3)" transform="matrix(1 0 0 1.00692 -44291.4 -47272.4)"><use width="100%" height="100%" xlink:href="#img2" transform="scale(6709.45 6709.45)"></use></g></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -2869,7 +2869,6 @@ a:link {
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
overflow-x: clip;
|
||||
min-height: fit-content;
|
||||
}
|
||||
|
||||
.uniqueIndexesContainer {
|
||||
|
||||
@@ -211,12 +211,3 @@ a:focus {
|
||||
.fileImportImg img {
|
||||
filter: brightness(0) saturate(100%);
|
||||
}
|
||||
|
||||
.tabPanesContainer {
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
min-height: 500px;
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
@@ -4,18 +4,13 @@ import * as React from "react";
|
||||
export interface TooltipProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
ariaLabelForTooltip?: string;
|
||||
}
|
||||
|
||||
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({
|
||||
children,
|
||||
className,
|
||||
ariaLabelForTooltip = children,
|
||||
}: TooltipProps) => {
|
||||
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children, className }: TooltipProps) => {
|
||||
return (
|
||||
<span className={className}>
|
||||
<TooltipHost content={children}>
|
||||
<Icon iconName="Info" aria-label={ariaLabelForTooltip} className="panelInfoIcon" tabIndex={0} />
|
||||
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -42,7 +42,6 @@ export interface IBulkDeleteResult {
|
||||
export const deleteDocuments = async (
|
||||
collection: CollectionBase,
|
||||
documentIds: DocumentId[],
|
||||
abortSignal: AbortSignal,
|
||||
): Promise<IBulkDeleteResult[]> => {
|
||||
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
||||
try {
|
||||
@@ -66,16 +65,12 @@ export const deleteDocuments = async (
|
||||
operationType: BulkOperationType.Delete,
|
||||
}));
|
||||
|
||||
const promise = v2Container.items
|
||||
.bulk(operations, undefined, {
|
||||
abortSignal,
|
||||
})
|
||||
.then((bulkResults) => {
|
||||
return bulkResults.map((bulkResult, index) => {
|
||||
const documentId = documentIdsChunk[index];
|
||||
return { ...bulkResult, documentId };
|
||||
});
|
||||
const promise = v2Container.items.bulk(operations).then((bulkResults) => {
|
||||
return bulkResults.map((bulkResult, index) => {
|
||||
const documentId = documentIdsChunk[index];
|
||||
return { ...bulkResult, documentId };
|
||||
});
|
||||
});
|
||||
promiseArray.push(promise);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FabricMessageTypes } from "./FabricMessageTypes";
|
||||
import { MessageTypes } from "./MessageTypes";
|
||||
|
||||
// This is the current version of these messages
|
||||
export const DATA_EXPLORER_RPC_VERSION = "3";
|
||||
@@ -20,19 +19,6 @@ export type DataExploreMessageV3 =
|
||||
type: FabricMessageTypes.GetAllResourceTokens;
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: FabricMessageTypes.GetAccessToken;
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: MessageTypes.TelemetryInfo;
|
||||
data: {
|
||||
action: string;
|
||||
actionModifier: string;
|
||||
data: unknown;
|
||||
timestamp: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: FabricMessageTypes.OpenSettings;
|
||||
params: [{ settingsId?: "About" | "Connection" }];
|
||||
|
||||
@@ -210,7 +210,7 @@ export interface IndexingPolicy {
|
||||
export interface VectorIndex {
|
||||
path: string;
|
||||
type: "flat" | "diskANN" | "quantizedFlat";
|
||||
vectorIndexShardKey?: string[];
|
||||
diskANNShardKey?: string;
|
||||
indexingSearchListSize?: number;
|
||||
quantizationByteSize?: number;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
ItemDefinition,
|
||||
JSONObject,
|
||||
QueryMetrics,
|
||||
Resource,
|
||||
@@ -31,11 +30,8 @@ export interface UploadDetailsRecord {
|
||||
numFailed: number;
|
||||
numThrottled: number;
|
||||
errors: string[];
|
||||
resources?: ItemDefinition[];
|
||||
}
|
||||
|
||||
export type BulkInsertResult = Omit<UploadDetailsRecord, "fileName">;
|
||||
|
||||
export interface QueryResultsMetadata {
|
||||
hasMoreResults: boolean;
|
||||
firstItemIndex: number;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AuthType } from "AuthType";
|
||||
import { shallow } from "enzyme";
|
||||
import ko from "knockout";
|
||||
import { Features } from "Platform/Hosted/extractFeatures";
|
||||
import React from "react";
|
||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
@@ -252,7 +253,7 @@ describe("SettingsComponent", () => {
|
||||
it("should save throughput bucket changes when Save button is clicked", async () => {
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
throughputBucketsEnabled: true,
|
||||
features: { enableThroughputBuckets: true } as Features,
|
||||
authType: AuthType.AAD,
|
||||
});
|
||||
|
||||
|
||||
@@ -191,7 +191,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||
|
||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||
this.throughputBucketsEnabled = userContext.throughputBucketsEnabled;
|
||||
this.throughputBucketsEnabled =
|
||||
userContext.apiType === "SQL" &&
|
||||
userContext.features.enableThroughputBuckets &&
|
||||
userContext.authType === AuthType.AAD;
|
||||
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
this.isFixedContainer =
|
||||
@@ -1071,11 +1074,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
databaseId: this.collection.databaseId,
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.collection.offer?.()?.autoscaleMaxThroughput
|
||||
? this.collection.offer?.()?.autoscaleMaxThroughput
|
||||
autopilotThroughput: this.collection.offer().autoscaleMaxThroughput
|
||||
? this.collection.offer().autoscaleMaxThroughput
|
||||
: undefined,
|
||||
manualThroughput: this.collection.offer?.()?.manualThroughput
|
||||
? this.collection.offer?.()?.manualThroughput
|
||||
manualThroughput: this.collection.offer().manualThroughput
|
||||
? this.collection.offer().manualThroughput
|
||||
: undefined,
|
||||
throughputBuckets: this.state.throughputBuckets,
|
||||
});
|
||||
@@ -1212,7 +1215,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isFullTextSearchEnabled: this.isFullTextSearchEnabled,
|
||||
shouldDiscardContainerPolicies: this.state.shouldDiscardContainerPolicies,
|
||||
resetShouldDiscardContainerPolicyChange: this.resetShouldDiscardContainerPolicies,
|
||||
isGlobalSecondaryIndex: this.isGlobalSecondaryIndex,
|
||||
};
|
||||
|
||||
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
|
||||
@@ -1341,7 +1343,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
});
|
||||
}
|
||||
|
||||
if (this.throughputBucketsEnabled && !hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||
if (this.throughputBucketsEnabled) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.ThroughputBucketsTab,
|
||||
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,
|
||||
|
||||
@@ -22,7 +22,6 @@ export interface ContainerPolicyComponentProps {
|
||||
isFullTextSearchEnabled: boolean;
|
||||
shouldDiscardContainerPolicies: boolean;
|
||||
resetShouldDiscardContainerPolicyChange: () => void;
|
||||
isGlobalSecondaryIndex?: boolean;
|
||||
}
|
||||
|
||||
export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> = ({
|
||||
|
||||
@@ -143,39 +143,4 @@ describe("SubSettingsComponent", () => {
|
||||
expect(subSettingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn);
|
||||
expect(subSettingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff);
|
||||
});
|
||||
|
||||
it("uniqueKey is visible", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableSQL" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const subSettingsComponent = new SubSettingsComponent(baseProps);
|
||||
expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(true);
|
||||
});
|
||||
|
||||
it("uniqueKey not visible due to no keys", () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
...(baseProps.collection.rawDataModel.uniqueKeyPolicy.uniqueKeys = []),
|
||||
};
|
||||
const subSettingsComponent = new SubSettingsComponent(props);
|
||||
expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(false);
|
||||
});
|
||||
|
||||
it("uniqueKey not visible for API", () => {
|
||||
const newContainer = new Explorer();
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableMongo" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
const props = { ...baseProps, container: newContainer };
|
||||
const subSettingsComponent = new SubSettingsComponent(props);
|
||||
expect(subSettingsComponent.getUniqueKeyVisible()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,16 +63,12 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
private geospatialVisible: boolean;
|
||||
private partitionKeyValue: string;
|
||||
private partitionKeyName: string;
|
||||
private uniqueKeyName: string;
|
||||
private uniqueKeyValue: string;
|
||||
|
||||
constructor(props: SubSettingsComponentProps) {
|
||||
super(props);
|
||||
this.geospatialVisible = userContext.apiType === "SQL";
|
||||
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||
this.partitionKeyValue = this.getPartitionKeyValue();
|
||||
this.uniqueKeyName = "Unique keys";
|
||||
this.uniqueKeyValue = this.getUniqueKeyValue();
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -355,28 +351,6 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
public isLargePartitionKeyEnabled = (): boolean => this.props.collection.partitionKey?.version >= 2;
|
||||
public isHierarchicalPartitionedContainer = (): boolean => this.props.collection.partitionKey?.kind === "MultiHash";
|
||||
|
||||
public getUniqueKeyVisible = (): boolean => {
|
||||
return this.props.collection.rawDataModel.uniqueKeyPolicy?.uniqueKeys.length > 0 && userContext.apiType === "SQL";
|
||||
};
|
||||
|
||||
private getUniqueKeyValue = (): string => {
|
||||
const paths = this.props.collection.rawDataModel.uniqueKeyPolicy?.uniqueKeys?.[0]?.paths;
|
||||
return paths?.join(", ") || "";
|
||||
};
|
||||
|
||||
private getUniqueKeyComponent = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
{this.getUniqueKeyVisible() && (
|
||||
<TextField
|
||||
label={this.uniqueKeyName}
|
||||
disabled
|
||||
styles={getTextFieldStyles(undefined, undefined)}
|
||||
defaultValue={this.uniqueKeyValue}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack {...subComponentStackProps}>
|
||||
@@ -389,8 +363,6 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
{this.props.changeFeedPolicyVisible && this.getChangeFeedComponent()}
|
||||
|
||||
{this.getPartitionKeyComponent()}
|
||||
|
||||
{this.getUniqueKeyComponent()}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("ThroughputBucketsComponent", () => {
|
||||
|
||||
it("renders the correct number of buckets", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(5);
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("renders buckets in the correct order even if input is unordered", () => {
|
||||
@@ -36,14 +36,8 @@ describe("ThroughputBucketsComponent", () => {
|
||||
];
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={unorderedBuckets} />);
|
||||
|
||||
const bucketLabels = screen.getAllByText(/Bucket \d+/).map((el) => el.textContent);
|
||||
expect(bucketLabels).toEqual([
|
||||
"Bucket 1 (Data Explorer Query Bucket)",
|
||||
"Bucket 2",
|
||||
"Bucket 3",
|
||||
"Bucket 4",
|
||||
"Bucket 5",
|
||||
]);
|
||||
const bucketLabels = screen.getAllByText(/Group \d+/).map((el) => el.textContent);
|
||||
expect(bucketLabels).toEqual(["Group 1 (Data Explorer Query Bucket)", "Group 2", "Group 3", "Group 4", "Group 5"]);
|
||||
});
|
||||
|
||||
it("renders all provided buckets even if they exceed the max default bucket count", () => {
|
||||
@@ -59,7 +53,7 @@ describe("ThroughputBucketsComponent", () => {
|
||||
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={oversizedBuckets} />);
|
||||
|
||||
expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(7);
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(7);
|
||||
|
||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("60")).toBeInTheDocument();
|
||||
@@ -177,7 +171,7 @@ describe("ThroughputBucketsComponent", () => {
|
||||
|
||||
it("ensures default buckets are used when no buckets are provided", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={[]} />);
|
||||
expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(5);
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
||||
expect(screen.getAllByDisplayValue("100")).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,7 +76,7 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
||||
value={bucket.maxThroughputPercentage}
|
||||
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
|
||||
showValue={false}
|
||||
label={`Bucket ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
|
||||
label={`Group ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
|
||||
styles={{ root: { flex: 2, maxWidth: 400 } }}
|
||||
disabled={bucket.maxThroughputPercentage === 100}
|
||||
/>
|
||||
|
||||
@@ -285,7 +285,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
serverId,
|
||||
numberOfRegions,
|
||||
isMultimaster,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -231,34 +231,6 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
defaultValue="/id"
|
||||
disabled={true}
|
||||
label="Unique keys"
|
||||
styles={
|
||||
{
|
||||
"fieldGroup": {
|
||||
"borderColor": "",
|
||||
"height": 25,
|
||||
"selectors": {
|
||||
":disabled": {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": undefined,
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
@@ -548,34 +520,6 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
defaultValue="/id"
|
||||
disabled={true}
|
||||
label="Unique keys"
|
||||
styles={
|
||||
{
|
||||
"fieldGroup": {
|
||||
"borderColor": "",
|
||||
"height": 25,
|
||||
"selectors": {
|
||||
":disabled": {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": undefined,
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
@@ -825,34 +769,6 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
defaultValue="/id"
|
||||
disabled={true}
|
||||
label="Unique keys"
|
||||
styles={
|
||||
{
|
||||
"fieldGroup": {
|
||||
"borderColor": "",
|
||||
"height": 25,
|
||||
"selectors": {
|
||||
":disabled": {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": undefined,
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
@@ -1167,34 +1083,6 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
defaultValue="/id"
|
||||
disabled={true}
|
||||
label="Unique keys"
|
||||
styles={
|
||||
{
|
||||
"fieldGroup": {
|
||||
"borderColor": "",
|
||||
"height": 25,
|
||||
"selectors": {
|
||||
":disabled": {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": undefined,
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
@@ -1483,33 +1371,5 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
defaultValue="/id"
|
||||
disabled={true}
|
||||
label="Unique keys"
|
||||
styles={
|
||||
{
|
||||
"fieldGroup": {
|
||||
"borderColor": "",
|
||||
"height": 25,
|
||||
"selectors": {
|
||||
":disabled": {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": undefined,
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
@@ -17,15 +17,7 @@ export const collection = {
|
||||
includedPaths: [],
|
||||
excludedPaths: [],
|
||||
}),
|
||||
rawDataModel: {
|
||||
uniqueKeyPolicy: {
|
||||
uniqueKeys: [
|
||||
{
|
||||
paths: ["/id"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
||||
usageSizeInKB: ko.observable(100),
|
||||
offer: ko.observable<DataModels.Offer>({
|
||||
autoscaleMaxThroughput: undefined,
|
||||
|
||||
@@ -71,18 +71,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyProperties": [
|
||||
"partitionKey",
|
||||
],
|
||||
"rawDataModel": {
|
||||
"uniqueKeyPolicy": {
|
||||
"uniqueKeys": [
|
||||
{
|
||||
"paths": [
|
||||
"/id",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": {},
|
||||
"usageSizeInKB": [Function],
|
||||
"vectorEmbeddingPolicy": [Function],
|
||||
}
|
||||
@@ -163,18 +153,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyProperties": [
|
||||
"partitionKey",
|
||||
],
|
||||
"rawDataModel": {
|
||||
"uniqueKeyPolicy": {
|
||||
"uniqueKeys": [
|
||||
{
|
||||
"paths": [
|
||||
"/id",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": {},
|
||||
"usageSizeInKB": [Function],
|
||||
"vectorEmbeddingPolicy": [Function],
|
||||
}
|
||||
@@ -294,18 +274,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyProperties": [
|
||||
"partitionKey",
|
||||
],
|
||||
"rawDataModel": {
|
||||
"uniqueKeyPolicy": {
|
||||
"uniqueKeys": [
|
||||
{
|
||||
"paths": [
|
||||
"/id",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": {},
|
||||
"usageSizeInKB": [Function],
|
||||
"vectorEmbeddingPolicy": [Function],
|
||||
}
|
||||
@@ -434,18 +404,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyProperties": [
|
||||
"partitionKey",
|
||||
],
|
||||
"rawDataModel": {
|
||||
"uniqueKeyPolicy": {
|
||||
"uniqueKeys": [
|
||||
{
|
||||
"paths": [
|
||||
"/id",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": {},
|
||||
"usageSizeInKB": [Function],
|
||||
"vectorEmbeddingPolicy": [Function],
|
||||
}
|
||||
|
||||
@@ -44,18 +44,13 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
||||
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier);
|
||||
const estimatedMonthlyCost = "Estimated monthly cost";
|
||||
|
||||
const iconWithEstimatedCostDisclaimer: JSX.Element = (
|
||||
<InfoTooltip ariaLabelForTooltip={`${estimatedMonthlyCost} ${currency} ${estimatedCostDisclaimer}`}>
|
||||
{estimatedCostDisclaimer}
|
||||
</InfoTooltip>
|
||||
);
|
||||
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>{estimatedCostDisclaimer}</InfoTooltip>;
|
||||
|
||||
if (isAutoscale) {
|
||||
return (
|
||||
<Text variant="small">
|
||||
{estimatedMonthlyCost} ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
|
||||
Estimated monthly cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
|
||||
<b>
|
||||
{currencySign + calculateEstimateNumber(monthlyPrice / 10)} -{" "}
|
||||
{currencySign + calculateEstimateNumber(monthlyPrice)}{" "}
|
||||
|
||||
@@ -345,13 +345,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
role="none"
|
||||
>
|
||||
<StyledIconBase
|
||||
aria-label="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
|
||||
ariaLabel="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
>
|
||||
<IconBase
|
||||
aria-label="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
|
||||
ariaLabel="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
styles={[Function]}
|
||||
@@ -1358,13 +1358,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
role="none"
|
||||
>
|
||||
<StyledIconBase
|
||||
aria-label="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
|
||||
ariaLabel="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
>
|
||||
<IconBase
|
||||
aria-label="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
|
||||
ariaLabel="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
styles={[Function]}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Stack,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import {
|
||||
@@ -30,7 +29,6 @@ export interface IVectorEmbeddingPoliciesComponentProps {
|
||||
discardChanges?: boolean;
|
||||
onChangesDiscarded?: () => void;
|
||||
disabled?: boolean;
|
||||
isGlobalSecondaryIndex?: boolean;
|
||||
}
|
||||
|
||||
export interface VectorEmbeddingPolicyData {
|
||||
@@ -41,7 +39,8 @@ export interface VectorEmbeddingPolicyData {
|
||||
indexType: VectorIndex["type"] | "none";
|
||||
pathError: string;
|
||||
dimensionsError: string;
|
||||
vectorIndexShardKey?: string[];
|
||||
diskANNShardKey?: string;
|
||||
diskANNShardKeyError?: string;
|
||||
indexingSearchListSize?: number;
|
||||
indexingSearchListSizeError?: string;
|
||||
quantizationByteSize?: number;
|
||||
@@ -88,7 +87,6 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
discardChanges,
|
||||
onChangesDiscarded,
|
||||
disabled,
|
||||
isGlobalSecondaryIndex,
|
||||
}): JSX.Element => {
|
||||
const onVectorEmbeddingPathError = (path: string, index?: number): string => {
|
||||
let error = "";
|
||||
@@ -134,6 +132,12 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
return error;
|
||||
};
|
||||
|
||||
//TODO: no restrictions yet due to this field being removed for now.
|
||||
// Uncomment and replace with validation code when field is reinstated
|
||||
// const onDiskANNShardKeyError = (shardKey: string): string => {
|
||||
// return "";
|
||||
// };
|
||||
|
||||
const initializeData = (vectorEmbeddings: VectorEmbedding[], vectorIndexes: VectorIndex[]) => {
|
||||
const mergedData: VectorEmbeddingPolicyData[] = [];
|
||||
vectorEmbeddings.forEach((embedding) => {
|
||||
@@ -143,7 +147,6 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
indexType: matchingIndex?.type || "none",
|
||||
indexingSearchListSize: matchingIndex?.indexingSearchListSize || undefined,
|
||||
quantizationByteSize: matchingIndex?.quantizationByteSize || undefined,
|
||||
vectorIndexShardKey: matchingIndex?.vectorIndexShardKey || undefined,
|
||||
pathError: onVectorEmbeddingPathError(embedding.path),
|
||||
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
|
||||
});
|
||||
@@ -183,7 +186,6 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
type: policy.indexType,
|
||||
indexingSearchListSize: policy.indexingSearchListSize,
|
||||
quantizationByteSize: policy.quantizationByteSize,
|
||||
vectorIndexShardKey: policy.vectorIndexShardKey,
|
||||
}) as VectorIndex,
|
||||
);
|
||||
const validationPassed = vectorEmbeddingPolicyData.every(
|
||||
@@ -245,16 +247,20 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const onShardKeyChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value.trim();
|
||||
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
if (!vectorEmbeddings[index]?.vectorIndexShardKey?.[0] && !value.startsWith("/")) {
|
||||
vectorEmbeddings[index].vectorIndexShardKey = ["/" + value];
|
||||
} else {
|
||||
vectorEmbeddings[index].vectorIndexShardKey = [value];
|
||||
}
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
// TODO: uncomment after Ignite
|
||||
// DiskANNShardKey was removed for Ignite due to backend problems. Leaving this here as it will be reinstated immediately after Ignite
|
||||
// const onDiskANNShardKeyChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// const value = event.target.value.trim();
|
||||
// const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||
// if (!vectorEmbeddings[index]?.diskANNShardKey && !value.startsWith("/")) {
|
||||
// vectorEmbeddings[index].diskANNShardKey = "/" + value;
|
||||
// } else {
|
||||
// vectorEmbeddings[index].diskANNShardKey = value;
|
||||
// }
|
||||
// const error = onDiskANNShardKeyError(value);
|
||||
// vectorEmbeddings[index].diskANNShardKeyError = error;
|
||||
// setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
// }
|
||||
|
||||
const onVectorEmbeddingPolicyChange = (
|
||||
index: number,
|
||||
@@ -286,11 +292,6 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||
};
|
||||
|
||||
const getQuantizationByteSizeTooltipContent = (): string => {
|
||||
const containerName: string = isGlobalSecondaryIndex ? "global secondary index" : "container";
|
||||
return `This is dynamically set by the ${containerName} if left blank, or it can be set to a fixed number`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 4 }}>
|
||||
{vectorEmbeddingPolicyData &&
|
||||
@@ -401,7 +402,6 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
styles={labelStyles}
|
||||
>
|
||||
Quantization byte size
|
||||
<InfoTooltip>{getQuantizationByteSizeTooltipContent()}</InfoTooltip>
|
||||
</Label>
|
||||
<TextField
|
||||
disabled={
|
||||
@@ -431,18 +431,26 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack style={{ marginLeft: "10px" }}>
|
||||
<Label disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"} styles={labelStyles}>
|
||||
Vector index shard key
|
||||
</Label>
|
||||
{/*TODO: uncomment after Ignite */}
|
||||
{/* DiskANNShardKey was removed for Ignite due to backend problems. Leaving this here as it will be reinstated immediately after Ignite
|
||||
<Stack
|
||||
style={{ marginLeft: "10px" }}
|
||||
>
|
||||
<Label
|
||||
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"}
|
||||
styles={labelStyles}
|
||||
>DiskANN shard key</Label>
|
||||
<TextField
|
||||
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"}
|
||||
id={`vector-policy-vectorIndexShardKey-${index + 1}`}
|
||||
id={`vector-policy-diskANNShardKey-${index + 1}`}
|
||||
styles={textFieldStyles}
|
||||
value={String(vectorEmbeddingPolicy.vectorIndexShardKey?.[0] ?? "")}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onShardKeyChange(index, event)}
|
||||
value={String(vectorEmbeddingPolicy.diskANNShardKey || "")}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onDiskANNShardKeyChange(index, event)
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
*/}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -12,7 +12,6 @@ import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } fro
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||
import { featureRegistered } from "Utils/FeatureRegistrationUtils";
|
||||
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import * as ko from "knockout";
|
||||
@@ -71,7 +70,6 @@ import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
|
||||
import StoredProcedure from "./Tree/StoredProcedure";
|
||||
import { useDatabases } from "./useDatabases";
|
||||
import { useSelectedNode } from "./useSelectedNode";
|
||||
import { UploadDetailsRecord } from "../Contracts/ViewModels";
|
||||
|
||||
BindingHandlersRegisterer.registerBindingHandlers();
|
||||
|
||||
@@ -284,42 +282,6 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public openInVsCode(): void {
|
||||
const activeTab = useTabs.getState().activeTab;
|
||||
const resourceId = encodeURIComponent(userContext.databaseAccount.id);
|
||||
const database = encodeURIComponent(activeTab?.collection?.databaseId);
|
||||
const container = encodeURIComponent(activeTab?.collection?.id());
|
||||
const baseUrl = `vscode://ms-azuretools.vscode-cosmosdb?resourceId=${resourceId}`;
|
||||
const vscodeUrl = activeTab ? `${baseUrl}&database=${database}&container=${container}` : baseUrl;
|
||||
|
||||
const openVSCodeDialogProps: DialogProps = {
|
||||
linkProps: {
|
||||
linkText: "Download Visual Studio Code",
|
||||
linkUrl: "https://code.visualstudio.com/download",
|
||||
},
|
||||
isModal: true,
|
||||
title: `Open your Azure Cosmos DB account in Visual Studio Code`,
|
||||
subText: `Please ensure Visual Studio Code is installed on your device.
|
||||
If you don't have it installed, please download it from the link below.`,
|
||||
primaryButtonText: "Open in VS Code",
|
||||
secondaryButtonText: "Cancel",
|
||||
|
||||
onPrimaryButtonClick: () => {
|
||||
try {
|
||||
window.location.href = vscodeUrl;
|
||||
TelemetryProcessor.traceStart(Action.OpenVSCode);
|
||||
} catch (error) {
|
||||
logConsoleError(`Failed to open VS Code: ${getErrorMessage(error)}`);
|
||||
}
|
||||
},
|
||||
onSecondaryButtonClick: () => {
|
||||
useDialog.getState().closeDialog();
|
||||
TelemetryProcessor.traceCancel(Action.OpenVSCode);
|
||||
},
|
||||
};
|
||||
useDialog.getState().openDialog(openVSCodeDialogProps);
|
||||
}
|
||||
|
||||
public async openCESCVAFeedbackBlade(): Promise<void> {
|
||||
sendMessage({ type: MessageTypes.OpenCESCVAFeedbackBlade });
|
||||
Logger.logInfo(
|
||||
@@ -1116,8 +1078,8 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public openUploadItemsPane(onUpload?: (data: UploadDetailsRecord[]) => void): void {
|
||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane onUpload={onUpload} />);
|
||||
public openUploadItemsPane(): void {
|
||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
|
||||
}
|
||||
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
||||
useSidePanel
|
||||
@@ -1125,7 +1087,7 @@ export default class Explorer {
|
||||
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
||||
}
|
||||
|
||||
public getDownloadModalContent(fileName: string): JSX.Element {
|
||||
public getDownloadModalConent(fileName: string): JSX.Element {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
return (
|
||||
<>
|
||||
@@ -1171,11 +1133,6 @@ export default class Explorer {
|
||||
await this.initNotebooks(userContext.databaseAccount);
|
||||
}
|
||||
|
||||
if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL") {
|
||||
const throughputBucketsEnabled = await featureRegistered(userContext.subscriptionId, "ThroughputBucketing");
|
||||
updateUserContext({ throughputBucketsEnabled });
|
||||
}
|
||||
|
||||
this.refreshSampleData();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
|
||||
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
|
||||
import SettingsIcon from "../../../../images/settings_15x15.svg";
|
||||
import SynapseIcon from "../../../../images/synapse-link.svg";
|
||||
import VSCodeIcon from "../../../../images/vscode.svg";
|
||||
import { AuthType } from "../../../AuthType";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { Platform, configContext } from "../../../ConfigContext";
|
||||
@@ -61,10 +60,6 @@ export function createStaticCommandBarButtons(
|
||||
addDivider();
|
||||
buttons.push(addSynapseLink);
|
||||
}
|
||||
if (userContext.apiType !== "Gremlin") {
|
||||
const addVsCode = createOpenVsCodeDialogButton(container);
|
||||
buttons.push(addVsCode);
|
||||
}
|
||||
}
|
||||
|
||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||
@@ -273,18 +268,6 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenVsCodeDialogButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Visual Studio Code";
|
||||
return {
|
||||
iconSrc: VSCodeIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => container.openInVsCode(),
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
function createLoginForEntraIDButton(container: Explorer): CommandButtonComponentProps {
|
||||
if (configContext.platform !== Platform.Portal) {
|
||||
return undefined;
|
||||
@@ -517,6 +500,6 @@ export function createPostgreButtons(container: Explorer): CommandButtonComponen
|
||||
|
||||
export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo);
|
||||
const addVsCode = createOpenVsCodeDialogButton(container);
|
||||
return [openVCoreMongoTerminalButton, addVsCode];
|
||||
|
||||
return [openVCoreMongoTerminalButton];
|
||||
}
|
||||
|
||||
@@ -175,6 +175,11 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
return false;
|
||||
}
|
||||
|
||||
if (globalSecondaryIndexThroughput > CollectionCreation.MaxRUPerPartition) {
|
||||
setErrorMessage("Unsharded collections support up to 10,000 RUs");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (showVectorSearchParameters()) {
|
||||
if (!vectorPolicyValidated) {
|
||||
setErrorMessage("Please fix errors in container vector policy");
|
||||
@@ -383,7 +388,6 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
setVectorIndexingPolicy,
|
||||
vectorPolicyValidated,
|
||||
setVectorPolicyValidated,
|
||||
isGlobalSecondaryIndex: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const ThroughputComponent = (props: ThroughputComponentProps): JSX.Elemen
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !useDatabases.getState().isFirstResourceCreated()}
|
||||
isDatabase={false}
|
||||
isSharded={true}
|
||||
isSharded={false}
|
||||
isFreeTier={isFreeTierAccount()}
|
||||
isQuickstart={false}
|
||||
isGlobalSecondaryIndex={true}
|
||||
|
||||
@@ -14,7 +14,6 @@ export interface VectorSearchComponentProps {
|
||||
vectorIndexingPolicy: VectorIndex[];
|
||||
setVectorIndexingPolicy: React.Dispatch<React.SetStateAction<VectorIndex[]>>;
|
||||
setVectorPolicyValidated: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isGlobalSecondaryIndex?: boolean;
|
||||
}
|
||||
|
||||
export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.Element => {
|
||||
@@ -24,7 +23,6 @@ export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.El
|
||||
vectorIndexingPolicy,
|
||||
setVectorIndexingPolicy,
|
||||
setVectorPolicyValidated,
|
||||
isGlobalSecondaryIndex,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -51,7 +49,6 @@ export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.El
|
||||
setVectorIndexingPolicy(vectorIndexingPolicy);
|
||||
setVectorPolicyValidated(vectorPolicyValidated);
|
||||
}}
|
||||
isGlobalSecondaryIndex={isGlobalSecondaryIndex}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -608,7 +608,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
<RightPaneForm {...genericPaneProps}>
|
||||
<div className={`paneMainContent ${styles.container}`}>
|
||||
{!isFabricNative() && (
|
||||
<Accordion className={`customAccordion ${styles.firstItem}`} collapsible>
|
||||
<Accordion className={`customAccordion ${styles.firstItem}`}>
|
||||
{shouldShowQueryPageOptions && (
|
||||
<AccordionItem value="1">
|
||||
<AccordionHeader>
|
||||
|
||||
@@ -12,7 +12,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
>
|
||||
<Accordion
|
||||
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
||||
collapsible={true}
|
||||
>
|
||||
<AccordionItem
|
||||
value="1"
|
||||
@@ -574,7 +573,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
>
|
||||
<Accordion
|
||||
className="customAccordion ___1uf6361_0000000 fz7g6wx"
|
||||
collapsible={true}
|
||||
>
|
||||
<AccordionItem
|
||||
value="7"
|
||||
|
||||
@@ -356,7 +356,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
value=""
|
||||
>
|
||||
<div
|
||||
className="ms-TextField is-required root-116"
|
||||
className="ms-TextField is-required root-110"
|
||||
>
|
||||
<div
|
||||
className="ms-TextField-wrapper"
|
||||
@@ -647,7 +647,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
}
|
||||
>
|
||||
<label
|
||||
className="ms-Label root-127"
|
||||
className="ms-Label root-121"
|
||||
htmlFor="TextField0"
|
||||
id="TextFieldLabel2"
|
||||
>
|
||||
@@ -656,13 +656,13 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
</LabelBase>
|
||||
</StyledLabelBase>
|
||||
<div
|
||||
className="ms-TextField-fieldGroup fieldGroup-117"
|
||||
className="ms-TextField-fieldGroup fieldGroup-111"
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-labelledby="TextFieldLabel2"
|
||||
autoFocus={true}
|
||||
className="ms-TextField-field field-118"
|
||||
className="ms-TextField-field field-112"
|
||||
id="TextField0"
|
||||
name="collectionIdConfirmation"
|
||||
onBlur={[Function]}
|
||||
@@ -2464,7 +2464,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="Create"
|
||||
className="ms-Button ms-Button--primary root-128"
|
||||
className="ms-Button ms-Button--primary root-122"
|
||||
data-is-focusable={true}
|
||||
data-test="Panel/OkButton"
|
||||
id="sidePanelOkButton"
|
||||
@@ -2477,14 +2477,14 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-flexContainer flexContainer-129"
|
||||
className="ms-Button-flexContainer flexContainer-123"
|
||||
data-automationid="splitbuttonprimary"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-textContainer textContainer-130"
|
||||
className="ms-Button-textContainer textContainer-124"
|
||||
>
|
||||
<span
|
||||
className="ms-Button-label label-132"
|
||||
className="ms-Button-label label-126"
|
||||
id="id__5"
|
||||
key="id__5"
|
||||
>
|
||||
|
||||
@@ -2,13 +2,9 @@ import {
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
DirectionalHint,
|
||||
FontIcon,
|
||||
IColumn,
|
||||
SelectionMode,
|
||||
TooltipHost,
|
||||
getTheme,
|
||||
mergeStyles,
|
||||
mergeStyleSets,
|
||||
} from "@fluentui/react";
|
||||
import { Upload } from "Common/Upload/Upload";
|
||||
import { UploadDetailsRecord } from "Contracts/ViewModels";
|
||||
@@ -18,41 +14,7 @@ import { getErrorMessage } from "../../Tables/Utilities";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
const theme = getTheme();
|
||||
const iconClass = mergeStyles({
|
||||
verticalAlign: "middle",
|
||||
maxHeight: "16px",
|
||||
maxWidth: "16px",
|
||||
});
|
||||
|
||||
const classNames = mergeStyleSets({
|
||||
fileIconHeaderIcon: {
|
||||
padding: 0,
|
||||
fontSize: "16px",
|
||||
},
|
||||
fileIconCell: {
|
||||
textAlign: "center",
|
||||
selectors: {
|
||||
"&:before": {
|
||||
content: ".",
|
||||
display: "inline-block",
|
||||
verticalAlign: "middle",
|
||||
height: "100%",
|
||||
width: "0px",
|
||||
visibility: "hidden",
|
||||
},
|
||||
},
|
||||
},
|
||||
error: [{ color: theme.semanticColors.errorIcon }, iconClass],
|
||||
accept: [{ color: theme.semanticColors.successIcon }, iconClass],
|
||||
warning: [{ color: theme.semanticColors.warningIcon }, iconClass],
|
||||
});
|
||||
|
||||
export type UploadItemsPaneProps = {
|
||||
onUpload?: (data: UploadDetailsRecord[]) => void;
|
||||
};
|
||||
|
||||
export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpload }) => {
|
||||
export const UploadItemsPane: FunctionComponent = () => {
|
||||
const [files, setFiles] = useState<FileList>();
|
||||
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
@@ -75,8 +37,6 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
||||
(uploadDetails) => {
|
||||
setUploadFileData(uploadDetails.data);
|
||||
setFiles(undefined);
|
||||
// Emit the upload details to the parent component
|
||||
onUpload && onUpload(uploadDetails.data);
|
||||
},
|
||||
(error: Error) => {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
@@ -100,94 +60,44 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
||||
};
|
||||
|
||||
const columns: IColumn[] = [
|
||||
{
|
||||
key: "icons",
|
||||
name: "",
|
||||
fieldName: "",
|
||||
className: classNames.fileIconCell,
|
||||
iconClassName: classNames.fileIconHeaderIcon,
|
||||
isIconOnly: true,
|
||||
minWidth: 16,
|
||||
maxWidth: 16,
|
||||
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
|
||||
if (item.numFailed) {
|
||||
const errorList = (
|
||||
<ul
|
||||
aria-label={"error list"}
|
||||
style={{
|
||||
margin: "5px 0",
|
||||
paddingLeft: "20px",
|
||||
listStyleType: "disc", // Explicitly set to use bullets (dots)
|
||||
}}
|
||||
>
|
||||
{item.errors.map((error, i) => (
|
||||
<li key={i} style={{ display: "list-item" }}>
|
||||
{error}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
return (
|
||||
<TooltipHost
|
||||
content={errorList}
|
||||
id={`tooltip-${index}-${column.key}`}
|
||||
directionalHint={DirectionalHint.bottomAutoEdge}
|
||||
>
|
||||
<FontIcon iconName="Error" className={classNames.error} aria-label="error" />
|
||||
</TooltipHost>
|
||||
);
|
||||
} else if (item.numThrottled) {
|
||||
return <FontIcon iconName="Warning" className={classNames.warning} aria-label="warning" />;
|
||||
} else {
|
||||
return <FontIcon iconName="Accept" className={classNames.accept} aria-label="accept" />;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "fileName",
|
||||
name: "FILE NAME",
|
||||
fieldName: "fileName",
|
||||
minWidth: 120,
|
||||
minWidth: 140,
|
||||
maxWidth: 140,
|
||||
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
|
||||
const fieldContent = item.fileName;
|
||||
return (
|
||||
<TooltipHost
|
||||
content={fieldContent}
|
||||
id={`tooltip-${index}-${column.key}`}
|
||||
directionalHint={DirectionalHint.bottomAutoEdge}
|
||||
>
|
||||
{fieldContent}
|
||||
</TooltipHost>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
name: "STATUS",
|
||||
fieldName: "numSucceeded",
|
||||
minWidth: 120,
|
||||
minWidth: 140,
|
||||
maxWidth: 140,
|
||||
isRowHeader: true,
|
||||
isResizable: true,
|
||||
data: "string",
|
||||
isPadded: true,
|
||||
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
|
||||
const fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
|
||||
return (
|
||||
<TooltipHost
|
||||
content={fieldContent}
|
||||
id={`tooltip-${index}-${column.key}`}
|
||||
directionalHint={DirectionalHint.bottomAutoEdge}
|
||||
>
|
||||
{fieldContent}
|
||||
</TooltipHost>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const _renderItemColumn = (item: UploadDetailsRecord, index: number, column: IColumn) => {
|
||||
let fieldContent: string;
|
||||
const tooltipId = `tooltip-${index}-${column.key}`;
|
||||
|
||||
switch (column.key) {
|
||||
case "status":
|
||||
fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
|
||||
break;
|
||||
default:
|
||||
fieldContent = item.fileName;
|
||||
}
|
||||
return (
|
||||
<TooltipHost content={fieldContent} id={tooltipId} directionalHint={DirectionalHint.rightCenter}>
|
||||
{fieldContent}
|
||||
</TooltipHost>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
<div className="paneMainContent">
|
||||
@@ -205,6 +115,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
||||
<DetailsList
|
||||
items={uploadFileData}
|
||||
columns={columns}
|
||||
onRenderItemColumn={_renderItemColumn}
|
||||
selectionMode={SelectionMode.none}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
isHeaderVisible={true}
|
||||
|
||||
@@ -30,10 +30,8 @@ import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboard
|
||||
import { isFabric, isFabricMirrored, isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
|
||||
import { conditionalClass } from "Utils/StyleUtils";
|
||||
import { Allotment, AllotmentHandle } from "allotment";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import useZoomLevel from "hooks/useZoomLevel";
|
||||
import { debounce } from "lodash";
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
@@ -106,23 +104,6 @@ const useSidebarStyles = makeStyles({
|
||||
display: "flex",
|
||||
},
|
||||
},
|
||||
accessibleContent: {
|
||||
"@media (max-width: 420px)": {
|
||||
overflow: "scroll",
|
||||
},
|
||||
},
|
||||
minHeightResponsive: {
|
||||
"@media (max-width: 420px)": {
|
||||
minHeight: "400px",
|
||||
},
|
||||
},
|
||||
accessibleContentZoom: {
|
||||
overflow: "scroll",
|
||||
},
|
||||
|
||||
minHeightZoom: {
|
||||
minHeight: "400px",
|
||||
},
|
||||
});
|
||||
|
||||
interface GlobalCommandsProps {
|
||||
@@ -294,7 +275,6 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
||||
const [expandedSize, setExpandedSize] = React.useState(300);
|
||||
const hasSidebar = userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo";
|
||||
const allotment = useRef<AllotmentHandle>(null);
|
||||
const isZoomed = useZoomLevel();
|
||||
|
||||
const expand = useCallback(() => {
|
||||
if (!expanded) {
|
||||
@@ -345,23 +325,11 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
||||
|
||||
return (
|
||||
<div className="sidebarContainer">
|
||||
<Allotment
|
||||
ref={allotment}
|
||||
onChange={onChange}
|
||||
onDragEnd={onDragEnd}
|
||||
className={`resourceTreeAndTabs ${styles.accessibleContent} ${conditionalClass(
|
||||
isZoomed,
|
||||
styles.accessibleContentZoom,
|
||||
)}`}
|
||||
>
|
||||
<Allotment ref={allotment} onChange={onChange} onDragEnd={onDragEnd} className="resourceTreeAndTabs">
|
||||
{/* Collections Tree - Start */}
|
||||
{hasSidebar && (
|
||||
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
|
||||
<Allotment.Pane
|
||||
className={`${styles.minHeightResponsive} ${conditionalClass(isZoomed, styles.minHeightZoom)}`}
|
||||
minSize={24}
|
||||
preferredSize={250}
|
||||
>
|
||||
<Allotment.Pane minSize={24} preferredSize={250}>
|
||||
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
||||
<div className={styles.sidebarContainer}>
|
||||
{loading && (
|
||||
@@ -417,10 +385,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
||||
</CosmosFluentProvider>
|
||||
</Allotment.Pane>
|
||||
)}
|
||||
<Allotment.Pane
|
||||
className={`${styles.minHeightResponsive} ${conditionalClass(isZoomed, styles.minHeightZoom)}`}
|
||||
minSize={200}
|
||||
>
|
||||
<Allotment.Pane minSize={200}>
|
||||
<Tabs explorer={explorer} />
|
||||
</Allotment.Pane>
|
||||
</Allotment>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/**
|
||||
* Accordion top class
|
||||
*/
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
import { Link, makeStyles, tokens } from "@fluentui/react-components";
|
||||
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
|
||||
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
|
||||
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
||||
import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
|
||||
import { isFabricNative, isFabricNativeReadOnly, openSettingsConnectionTab } from "Platform/Fabric/FabricUtil";
|
||||
import * as React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg";
|
||||
import LinkIcon from "../../../images/Link_blue.svg";
|
||||
import Explorer from "../Explorer";
|
||||
|
||||
export interface SplashScreenProps {
|
||||
@@ -154,7 +155,7 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
title: "App development",
|
||||
description: "Start here to use an SDK to build your apps",
|
||||
icon: <LinkMultipleRegular />,
|
||||
onClick: () => window.open("https://aka.ms/cosmosdbfabricsdk", "_blank"),
|
||||
onClick: () => openSettingsConnectionTab(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -185,12 +186,12 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
{title}
|
||||
</div>
|
||||
{getSplashScreenButtons()}
|
||||
{/* <div className={styles.footer}>
|
||||
<div className={styles.footer}>
|
||||
Need help?{" "}
|
||||
<Link href="https://aka.ms/cosmosdbfabricdocs" target="_blank">
|
||||
Learn more <img src={LinkIcon} alt="Learn more" />
|
||||
</Link>
|
||||
</div> */}
|
||||
</div>
|
||||
</CosmosFluentProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -28,7 +28,6 @@ import LinkIcon from "../../../images/Link_blue.svg";
|
||||
import PowerShellIcon from "../../../images/PowerShell.svg";
|
||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
|
||||
import VisualStudioIcon from "../../../images/VisualStudio.svg";
|
||||
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -291,10 +290,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
<form className="connectExplorerFormContainer">
|
||||
<div className="splashScreenContainer">
|
||||
<div className="splashScreen">
|
||||
<h2 className="title" role="heading" aria-label={title}>
|
||||
<h1 className="title" role="heading" aria-label={title}>
|
||||
{title}
|
||||
<FeaturePanelLauncher />
|
||||
</h2>
|
||||
</h1>
|
||||
<div className="subtitle">{subtitle}</div>
|
||||
{this.getSplashScreenButtons()}
|
||||
{useCarousel.getState().showCoachMark && (
|
||||
@@ -459,10 +458,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
}
|
||||
|
||||
if (userContext.apiType === "VCoreMongo") {
|
||||
icon = VisualStudioIcon;
|
||||
title = "Connect with VS Code";
|
||||
description = "Query and Manage your MongoDB cluster in Visual Studio Code";
|
||||
onClick = () => this.container.openInVsCode();
|
||||
icon = ContainersIcon;
|
||||
title = "Connect with Studio 3T";
|
||||
description = "Prefer Studio 3T? Find your connection strings here";
|
||||
onClick = () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -43,52 +43,32 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
|
||||
await ensureCloudShellProviderRegistered();
|
||||
|
||||
resolvedRegion = determineCloudShellRegion();
|
||||
|
||||
resolvedRegion = determineCloudShellRegion();
|
||||
|
||||
terminal.writeln(formatWarningMessage("⚠️ IMPORTANT: Azure Cloud Shell Region Notice ⚠️"));
|
||||
terminal.writeln(
|
||||
formatInfoMessage(
|
||||
"The Cloud Shell environment will operate in a region that may differ from your database's region.",
|
||||
),
|
||||
);
|
||||
terminal.writeln(formatInfoMessage("This has two potential implications:"));
|
||||
terminal.writeln(formatInfoMessage("1. Performance Impact:"));
|
||||
terminal.writeln(
|
||||
formatInfoMessage(" Commands may experience higher latency due to geographic distance between regions."),
|
||||
);
|
||||
terminal.writeln(formatInfoMessage("2. Data Compliance Considerations:"));
|
||||
terminal.writeln(
|
||||
formatInfoMessage(
|
||||
" Data processed through this shell could temporarily reside in a different geographic region,",
|
||||
),
|
||||
);
|
||||
terminal.writeln(
|
||||
formatInfoMessage(" which may affect compliance with data residency requirements or regulations specific"),
|
||||
);
|
||||
terminal.writeln(formatInfoMessage(" to your organization."));
|
||||
terminal.writeln("");
|
||||
|
||||
terminal.writeln("\x1b[94mFor more information on Azure Cosmos DB data governance and compliance, please visit:");
|
||||
terminal.writeln("\x1b[94mhttps://learn.microsoft.com/en-us/azure/cosmos-db/data-residency\x1b[0m");
|
||||
|
||||
// Ask for user consent for region
|
||||
const consentGranted = await askConfirmation(terminal, formatWarningMessage("Do you wish to proceed?"));
|
||||
const consentGranted = await askConfirmation(
|
||||
terminal,
|
||||
formatWarningMessage(
|
||||
"The shell environment may be operating in a region different from that of the database, which could impact performance or data compliance. Do you wish to proceed?",
|
||||
),
|
||||
);
|
||||
|
||||
// Track user decision
|
||||
TelemetryProcessor.trace(
|
||||
Action.CloudShellUserConsent,
|
||||
consentGranted ? ActionModifiers.Success : ActionModifiers.Cancel,
|
||||
{
|
||||
dataExplorerArea: Areas.CloudShell,
|
||||
shellType: TerminalKind[shellType],
|
||||
isConsent: consentGranted,
|
||||
region: resolvedRegion,
|
||||
},
|
||||
startKey,
|
||||
{ dataExplorerArea: Areas.CloudShell },
|
||||
);
|
||||
|
||||
if (!consentGranted) {
|
||||
TelemetryProcessor.traceCancel(
|
||||
Action.CloudShellTerminalSession,
|
||||
{
|
||||
shellType: TerminalKind[shellType],
|
||||
dataExplorerArea: Areas.CloudShell,
|
||||
region: resolvedRegion,
|
||||
isConsent: false,
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
terminal.writeln(
|
||||
formatErrorMessage("Session ended. Please close this tab and initiate a new shell session if needed."),
|
||||
);
|
||||
@@ -282,27 +262,28 @@ export const configureSocketConnection = async (
|
||||
};
|
||||
|
||||
export const sendTerminalStartupCommands = (socket: WebSocket, initCommands: string): void => {
|
||||
// ensures connections don't remain open indefinitely by implementing an automatic timeout after 120 minutes.
|
||||
const keepSocketAlive = (socket: WebSocket) => {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
if (pingCount >= MAX_PING_COUNT) {
|
||||
socket.close();
|
||||
} else {
|
||||
pingCount++;
|
||||
// The code uses a recursive setTimeout pattern rather than setInterval,
|
||||
// which ensures each new ping only happens after the previous one completes
|
||||
// and naturally stops if the socket closes.
|
||||
keepAliveID = setTimeout(() => keepSocketAlive(socket), 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(initCommands);
|
||||
keepSocketAlive(socket);
|
||||
} else {
|
||||
socket.onopen = () => {
|
||||
socket.send(initCommands);
|
||||
|
||||
// ensures connections don't remain open indefinitely by implementing an automatic timeout after 20 minutes.
|
||||
const keepSocketAlive = (socket: WebSocket) => {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
if (pingCount >= MAX_PING_COUNT) {
|
||||
socket.close();
|
||||
} else {
|
||||
socket.send("");
|
||||
pingCount++;
|
||||
// The code uses a recursive setTimeout pattern rather than setInterval,
|
||||
// which ensures each new ping only happens after the previous one completes
|
||||
// and naturally stops if the socket closes.
|
||||
keepAliveID = setTimeout(() => keepSocketAlive(socket), 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
keepSocketAlive(socket);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,12 +22,6 @@ export const EXIT_COMMAND = ` printf "\\033[1;31mSession ended. Please close thi
|
||||
* the required methods.
|
||||
*/
|
||||
export abstract class AbstractShellHandler {
|
||||
/**
|
||||
* The name of the application using this shell handler.
|
||||
* This is used for telemetry and logging purposes.
|
||||
*/
|
||||
protected APP_NAME = "CosmosExplorerTerminal";
|
||||
|
||||
abstract getShellName(): string;
|
||||
abstract getSetUpCommands(): string[];
|
||||
abstract getConnectionCommand(): string;
|
||||
@@ -62,30 +56,4 @@ export abstract class AbstractShellHandler {
|
||||
|
||||
return allCommands.join("\n").concat("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup commands for MongoDB shell:
|
||||
*
|
||||
* 1. Check if mongosh is already installed
|
||||
* 2. Download mongosh package if not installed
|
||||
* 3. Extract the package to access mongosh binaries
|
||||
* 4. Move extracted files to ~/mongosh directory
|
||||
* 5. Add mongosh binary path to system PATH
|
||||
* 6. Apply PATH changes by sourcing .bashrc
|
||||
*
|
||||
* Each command runs conditionally only if mongosh
|
||||
* is not already present in the environment.
|
||||
*/
|
||||
protected mongoShellSetupCommands(): string[] {
|
||||
const PACKAGE_VERSION: string = "2.5.0";
|
||||
return [
|
||||
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
||||
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||
`if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh/bin && mv mongosh-${PACKAGE_VERSION}-linux-x64/bin/mongosh ~/mongosh/bin/ && chmod +x ~/mongosh/bin/mongosh; fi`,
|
||||
`if ! command -v mongosh &> /dev/null; then rm -rf mongosh-${PACKAGE_VERSION}-linux-x64 mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
||||
"source ~/.bashrc",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ describe("CassandraShellHandler", () => {
|
||||
});
|
||||
|
||||
test("should return correct connection command", () => {
|
||||
const expectedCommand = `cqlsh test-endpoint.cassandra.cosmos.azure.com 10350 -u test-account -p test-key --ssl`;
|
||||
const expectedCommand = "cqlsh test-endpoint.cassandra.cosmos.azure.com 10350 -u test-account -p test-key --ssl";
|
||||
|
||||
expect(handler.getConnectionCommand()).toBe(expectedCommand);
|
||||
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-endpoint.cassandra.cosmos.azure.com:443/");
|
||||
|
||||
@@ -68,7 +68,7 @@ describe("MongoShellHandler", () => {
|
||||
const commands = mongoShellHandler.getSetUpCommands();
|
||||
|
||||
expect(Array.isArray(commands)).toBe(true);
|
||||
expect(commands.length).toBe(7);
|
||||
expect(commands.length).toBe(6);
|
||||
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
|
||||
});
|
||||
});
|
||||
@@ -91,7 +91,7 @@ describe("MongoShellHandler", () => {
|
||||
const command = mongoShellHandler.getConnectionCommand();
|
||||
|
||||
expect(command).toBe(
|
||||
"mongosh mongodb://test-mongo.documents.azure.com:10255?appName=CosmosExplorerTerminal --username test-account --password test-key --tls --tlsAllowInvalidCertificates",
|
||||
"mongosh --host test-mongo.documents.azure.com --port 10255 --username test-account --password test-key --tls --tlsAllowInvalidCertificates",
|
||||
);
|
||||
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-mongo.documents.azure.com:443/");
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import { userContext } from "../../../../UserContext";
|
||||
import { getHostFromUrl } from "../Utils/CommonUtils";
|
||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
||||
|
||||
const PACKAGE_VERSION: string = "2.5.0";
|
||||
|
||||
export class MongoShellHandler extends AbstractShellHandler {
|
||||
private _key: string;
|
||||
private _endpoint: string | undefined;
|
||||
@@ -16,7 +18,14 @@ export class MongoShellHandler extends AbstractShellHandler {
|
||||
}
|
||||
|
||||
public getSetUpCommands(): string[] {
|
||||
return this.mongoShellSetupCommands();
|
||||
return [
|
||||
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
||||
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||
`if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh && mv mongosh-${PACKAGE_VERSION}-linux-x64/* ~/mongosh/; fi`,
|
||||
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
||||
"source ~/.bashrc",
|
||||
];
|
||||
}
|
||||
|
||||
public getConnectionCommand(): string {
|
||||
@@ -28,17 +37,9 @@ export class MongoShellHandler extends AbstractShellHandler {
|
||||
if (!dbName) {
|
||||
return "echo 'Database name not found.'";
|
||||
}
|
||||
return (
|
||||
"mongosh mongodb://" +
|
||||
getHostFromUrl(this._endpoint) +
|
||||
":10255?appName=" +
|
||||
this.APP_NAME +
|
||||
" --username " +
|
||||
dbName +
|
||||
" --password " +
|
||||
this._key +
|
||||
" --tls --tlsAllowInvalidCertificates"
|
||||
);
|
||||
return `mongosh --host ${getHostFromUrl(this._endpoint)} --port 10255 --username ${dbName} --password ${
|
||||
this._key
|
||||
} --tls --tlsAllowInvalidCertificates`;
|
||||
}
|
||||
|
||||
public getTerminalSuppressedData(): string {
|
||||
|
||||
@@ -54,7 +54,7 @@ export class PostgresShellHandler extends AbstractShellHandler {
|
||||
// All Azure Cosmos DB PostgreSQL deployments follow this convention.
|
||||
// Ref. https://learn.microsoft.com/en-us/azure/cosmos-db/postgresql/reference-limits#database-creation
|
||||
const loginName = userContext.postgresConnectionStrParams.adminLogin;
|
||||
return `psql -h "${this._endpoint}" -p 5432 -d "citus" -U "${loginName}" --set=sslmode=require --set=application_name=${this.APP_NAME}`;
|
||||
return `psql -h "${this._endpoint}" -p 5432 -d "citus" -U "${loginName}" --set=sslmode=require`;
|
||||
}
|
||||
|
||||
public getTerminalSuppressedData(): string {
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("VCoreMongoShellHandler", () => {
|
||||
const commands = vcoreMongoShellHandler.getSetUpCommands();
|
||||
|
||||
expect(Array.isArray(commands)).toBe(true);
|
||||
expect(commands.length).toBe(7);
|
||||
expect(commands.length).toBe(6);
|
||||
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
|
||||
expect(commands[0]).toContain("mongosh not found");
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { userContext } from "../../../../UserContext";
|
||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
||||
|
||||
const PACKAGE_VERSION: string = "2.5.0";
|
||||
|
||||
export class VCoreMongoShellHandler extends AbstractShellHandler {
|
||||
private _endpoint: string | undefined;
|
||||
|
||||
@@ -13,8 +15,28 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
|
||||
return "MongoDB VCore";
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup commands for MongoDB VCore shell:
|
||||
*
|
||||
* 1. Check if mongosh is already installed
|
||||
* 2. Download mongosh package if not installed
|
||||
* 3. Extract the package to access mongosh binaries
|
||||
* 4. Move extracted files to ~/mongosh directory
|
||||
* 5. Add mongosh binary path to system PATH
|
||||
* 6. Apply PATH changes by sourcing .bashrc
|
||||
*
|
||||
* Each command runs conditionally only if mongosh
|
||||
* is not already present in the environment.
|
||||
*/
|
||||
public getSetUpCommands(): string[] {
|
||||
return this.mongoShellSetupCommands();
|
||||
return [
|
||||
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
||||
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||
`if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh && mv mongosh-${PACKAGE_VERSION}-linux-x64/* ~/mongosh/; fi`,
|
||||
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
||||
"source ~/.bashrc",
|
||||
];
|
||||
}
|
||||
|
||||
public getConnectionCommand(): string {
|
||||
@@ -23,7 +45,7 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
|
||||
}
|
||||
|
||||
const userName = userContext.vcoreMongoConnectionParams.adminLogin;
|
||||
return `mongosh "mongodb+srv://${userName}:@${this._endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000&appName=${this.APP_NAME}"`;
|
||||
return `mongosh "mongodb+srv://${userName}:@${this._endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000"`;
|
||||
}
|
||||
|
||||
public getTerminalSuppressedData(): string {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { AbstractShellHandler } from "Explorer/Tabs/CloudShellTab/ShellTypes/AbstractShellHandler";
|
||||
import { IDisposable, ITerminalAddon, Terminal } from "@xterm/xterm";
|
||||
import { AbstractShellHandler } from "../ShellTypes/AbstractShellHandler";
|
||||
import { formatErrorMessage } from "./TerminalLogFormats";
|
||||
|
||||
interface IAttachOptions {
|
||||
bidirectional?: boolean;
|
||||
@@ -57,27 +56,8 @@ export class AttachAddon implements ITerminalAddon {
|
||||
this._disposables.push(terminal.onBinary((data) => this._sendBinary(data)));
|
||||
}
|
||||
|
||||
this._disposables.push(addSocketListener(this._socket, "close", () => this._handleSocketClose(terminal)));
|
||||
this._disposables.push(addSocketListener(this._socket, "error", () => this._handleSocketClose(terminal)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles socket close events by terminating processes and showing a message
|
||||
*/
|
||||
private _handleSocketClose(terminal: Terminal): void {
|
||||
if (terminal) {
|
||||
terminal.writeln(
|
||||
formatErrorMessage("Session ended. Please close this tab and initiate a new shell session if needed."),
|
||||
);
|
||||
|
||||
// Send exit command to terminal
|
||||
if (this._bidirectional) {
|
||||
terminal.write(formatErrorMessage("exit\r\n"));
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up resources
|
||||
this.dispose();
|
||||
this._disposables.push(addSocketListener(this._socket, "close", () => this.dispose()));
|
||||
this._disposables.push(addSocketListener(this._socket, "error", () => this.dispose()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { InputDataList, InputDatalistDropdownOptionSection } from "Explorer/Controls/InputDataList/InputDataList";
|
||||
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import {
|
||||
@@ -63,7 +64,7 @@ import * as Logger from "../../../Common/Logger";
|
||||
import * as MongoProxyClient from "../../../Common/MongoProxyClient";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { CollectionBase, UploadDetailsRecord } from "../../../Contracts/ViewModels";
|
||||
import { CollectionBase } from "../../../Contracts/ViewModels";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||
import { defaultQueryFields, extractPartitionKeyValues } from "../../../Utils/QueryUtils";
|
||||
@@ -143,13 +144,6 @@ export const useDocumentsTabStyles = makeStyles({
|
||||
deleteProgressContent: {
|
||||
paddingTop: tokens.spacingVerticalL,
|
||||
},
|
||||
smallScreenContent: {
|
||||
"@media (max-width: 420px)": {
|
||||
flexWrap: "wrap",
|
||||
minHeight: "max-content",
|
||||
padding: "4px",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export class DocumentsTabV2 extends TabsBase {
|
||||
@@ -308,6 +302,7 @@ type UiKeyboardEvent = (e: KeyboardEvent | React.SyntheticEvent<Element, Event>)
|
||||
|
||||
// Export to expose to unit tests
|
||||
export type ButtonsDependencies = {
|
||||
_collection: ViewModels.CollectionBase;
|
||||
selectedRows: Set<TableRowId>;
|
||||
editorState: ViewModels.DocumentExplorerState;
|
||||
isPreferredApiMongoDB: boolean;
|
||||
@@ -318,7 +313,26 @@ export type ButtonsDependencies = {
|
||||
onSaveExistingDocumentClick: UiKeyboardEvent;
|
||||
onRevertExistingDocumentClick: UiKeyboardEvent;
|
||||
onDeleteExistingDocumentsClick: UiKeyboardEvent;
|
||||
onUploadDocumentsClick: UiKeyboardEvent;
|
||||
};
|
||||
|
||||
const createUploadButton = (container: Explorer): CommandButtonComponentProps => {
|
||||
const label = "Upload Item";
|
||||
return {
|
||||
id: UPLOAD_BUTTON_ID,
|
||||
iconSrc: UploadIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
|
||||
selectedCollection && container.openUploadItemsPane();
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled:
|
||||
useSelectedNode.getState().isDatabaseNodeOrNoneSelected() ||
|
||||
!useClientWriteEnabled.getState().clientWriteEnabled ||
|
||||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
};
|
||||
};
|
||||
|
||||
// Export to expose to unit tests
|
||||
@@ -331,6 +345,7 @@ export const UPLOAD_BUTTON_ID = "uploadItemBtn";
|
||||
|
||||
// Export to expose in unit tests
|
||||
export const getTabsButtons = ({
|
||||
_collection,
|
||||
selectedRows,
|
||||
editorState,
|
||||
isPreferredApiMongoDB,
|
||||
@@ -341,7 +356,6 @@ export const getTabsButtons = ({
|
||||
onSaveExistingDocumentClick,
|
||||
onRevertExistingDocumentClick,
|
||||
onDeleteExistingDocumentsClick,
|
||||
onUploadDocumentsClick,
|
||||
}: ButtonsDependencies): CommandButtonComponentProps[] => {
|
||||
if (isFabric() && userContext.fabricContext?.isReadOnly) {
|
||||
// All the following buttons require write access
|
||||
@@ -453,20 +467,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
if (!isPreferredApiMongoDB) {
|
||||
const label = "Upload Item";
|
||||
buttons.push({
|
||||
id: UPLOAD_BUTTON_ID,
|
||||
iconSrc: UploadIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: onUploadDocumentsClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled:
|
||||
useSelectedNode.getState().isDatabaseNodeOrNoneSelected() ||
|
||||
!useClientWriteEnabled.getState().clientWriteEnabled ||
|
||||
useSelectedNode.getState().isQueryCopilotCollectionSelected(),
|
||||
});
|
||||
buttons.push(createUploadButton(_collection.container));
|
||||
}
|
||||
|
||||
return buttons;
|
||||
@@ -671,7 +672,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
collection: CollectionBase;
|
||||
}>(undefined);
|
||||
const [bulkDeleteMode, setBulkDeleteMode] = useState<"inProgress" | "completed" | "aborting" | "aborted">(undefined);
|
||||
const [abortController, setAbortController] = useState<AbortController | undefined>(undefined);
|
||||
|
||||
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
||||
|
||||
@@ -699,19 +699,13 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
return;
|
||||
}
|
||||
|
||||
if (bulkDeleteProcess.pendingIds.length === 0 && bulkDeleteProcess.throttledIds.length === 0) {
|
||||
// Successfully deleted all documents
|
||||
if (
|
||||
(bulkDeleteProcess.pendingIds.length === 0 && bulkDeleteProcess.throttledIds.length === 0) ||
|
||||
bulkDeleteMode === "aborting"
|
||||
) {
|
||||
// Successfully deleted all documents or operation was aborted
|
||||
bulkDeleteOperation.onCompleted(bulkDeleteProcess.successfulIds);
|
||||
setBulkDeleteMode("completed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (bulkDeleteMode === "aborting") {
|
||||
// Operation was aborted
|
||||
abortController?.abort();
|
||||
bulkDeleteOperation.onCompleted(bulkDeleteProcess.successfulIds);
|
||||
setBulkDeleteMode("aborted");
|
||||
setAbortController(undefined);
|
||||
setBulkDeleteMode(bulkDeleteMode === "aborting" ? "aborted" : "completed");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -719,10 +713,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
const newPendingIds = bulkDeleteProcess.pendingIds.concat(bulkDeleteProcess.throttledIds);
|
||||
const timeout = bulkDeleteProcess.beforeExecuteMs || 0;
|
||||
|
||||
const ac = new AbortController();
|
||||
setAbortController(ac);
|
||||
setTimeout(() => {
|
||||
deleteNoSqlDocuments(bulkDeleteOperation.collection, [...newPendingIds], ac.signal)
|
||||
deleteNoSqlDocuments(bulkDeleteOperation.collection, [...newPendingIds])
|
||||
.then((deleteResult) => {
|
||||
let retryAfterMilliseconds = 0;
|
||||
const newSuccessful: DocumentId[] = [];
|
||||
@@ -737,7 +729,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
} else if (result.statusCode >= 400) {
|
||||
newFailed.push(result.documentId);
|
||||
logConsoleError(
|
||||
`Failed to delete document ${result.documentId.id()} with status code ${result.statusCode}`,
|
||||
`Failed to delete document ${result.documentId.id} with status code ${result.statusCode}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -878,6 +870,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
}
|
||||
|
||||
updateNavbarWithTabsButtons(isTabActive, {
|
||||
_collection,
|
||||
selectedRows,
|
||||
editorState,
|
||||
isPreferredApiMongoDB,
|
||||
@@ -888,7 +881,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
onSaveExistingDocumentClick,
|
||||
onRevertExistingDocumentClick,
|
||||
onDeleteExistingDocumentsClick,
|
||||
onUploadDocumentsClick,
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -1294,47 +1286,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
);
|
||||
}, [deleteDocuments, documentIds, isPreferredApiMongoDB, selectedRows]);
|
||||
|
||||
const onUploadDocumentsClick = useCallback((): void => {
|
||||
if (!isPreferredApiMongoDB) {
|
||||
const onSuccessUpload = (data: UploadDetailsRecord[]) => {
|
||||
const addedIdsSet = new Set(
|
||||
data
|
||||
.reduce(
|
||||
(result: ItemDefinition[], record) =>
|
||||
result.concat(record.resources && record.resources.length ? record.resources : []),
|
||||
[],
|
||||
)
|
||||
.map((document) => {
|
||||
const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues(
|
||||
document,
|
||||
partitionKey as PartitionKeyDefinition,
|
||||
);
|
||||
return newDocumentId(
|
||||
document as ItemDefinition & Resource,
|
||||
partitionKeyProperties,
|
||||
partitionKeyValueArray as string[],
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const documents = new Set(documentIds);
|
||||
addedIdsSet.forEach((item) => documents.add(item));
|
||||
setDocumentIds(Array.from(documents));
|
||||
|
||||
setSelectedDocumentContent(undefined);
|
||||
setClickedRowIndex(undefined);
|
||||
setSelectedRows(new Set());
|
||||
setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||
};
|
||||
|
||||
_collection.container.openUploadItemsPane(onSuccessUpload);
|
||||
}
|
||||
}, [_collection.container, documentIds, isPreferredApiMongoDB, newDocumentId, partitionKey, partitionKeyProperties]);
|
||||
|
||||
// If editor state changes, update the nav
|
||||
useEffect(
|
||||
() =>
|
||||
updateNavbarWithTabsButtons(isTabActive, {
|
||||
_collection,
|
||||
selectedRows,
|
||||
editorState,
|
||||
isPreferredApiMongoDB,
|
||||
@@ -1343,11 +1299,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
onSaveNewDocumentClick,
|
||||
onRevertNewDocumentClick,
|
||||
onSaveExistingDocumentClick,
|
||||
onRevertExistingDocumentClick,
|
||||
onDeleteExistingDocumentsClick,
|
||||
onUploadDocumentsClick,
|
||||
onRevertExistingDocumentClick: onRevertExistingDocumentClick,
|
||||
onDeleteExistingDocumentsClick: onDeleteExistingDocumentsClick,
|
||||
}),
|
||||
[
|
||||
_collection,
|
||||
selectedRows,
|
||||
editorState,
|
||||
isPreferredApiMongoDB,
|
||||
@@ -1358,7 +1314,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
onSaveExistingDocumentClick,
|
||||
onRevertExistingDocumentClick,
|
||||
onDeleteExistingDocumentsClick,
|
||||
onUploadDocumentsClick,
|
||||
isTabActive,
|
||||
],
|
||||
);
|
||||
@@ -2147,7 +2102,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
return (
|
||||
<CosmosFluentProvider className={styles.container}>
|
||||
<div data-test={"DocumentsTab"} className="tab-pane active" role="tabpanel" style={{ display: "flex" }}>
|
||||
<div data-test={"DocumentsTab/Filter"} className={`${styles.filterRow} ${styles.smallScreenContent}`}>
|
||||
<div data-test={"DocumentsTab/Filter"} className={styles.filterRow}>
|
||||
{!isPreferredApiMongoDB && <span> SELECT * FROM c </span>}
|
||||
<InputDataList
|
||||
dropdownOptions={getFilterChoices()}
|
||||
|
||||
@@ -15,7 +15,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29 ___1ngl8o6_0000000 fz7mnu6 fl3egqs flhmrkm"
|
||||
className="___11ktxfv_0000000 f1o614cb fy9rknc f22iagw fsnqrgy f1f5gg8d fjodcmx f122n59 f1f09k3d fg706s2 frpde29"
|
||||
data-test="DocumentsTab/Filter"
|
||||
>
|
||||
<span>
|
||||
|
||||
@@ -8,8 +8,6 @@ import RunQuery from "../../../../images/RunQuery.png";
|
||||
import { QueryResults } from "../../../Contracts/ViewModels";
|
||||
import { ErrorList } from "./ErrorList";
|
||||
import { ResultsView } from "./ResultsView";
|
||||
import useZoomLevel from "hooks/useZoomLevel";
|
||||
import { conditionalClass } from "Utils/StyleUtils";
|
||||
|
||||
export interface ResultsViewProps {
|
||||
isMongoDB: boolean;
|
||||
@@ -25,16 +23,11 @@ interface QueryResultProps extends ResultsViewProps {
|
||||
|
||||
const ExecuteQueryCallToAction: React.FC = () => {
|
||||
const styles = useQueryTabStyles();
|
||||
const isZoomed = useZoomLevel();
|
||||
return (
|
||||
<div data-test="QueryTab/ResultsPane/ExecuteCTA" className={styles.executeCallToAction}>
|
||||
<div>
|
||||
<p>
|
||||
<img
|
||||
className={`${styles.responsiveImg} ${conditionalClass(isZoomed, styles.zoomedImageSize)}`}
|
||||
src={RunQuery}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<img src={RunQuery} aria-hidden="true" />
|
||||
</p>
|
||||
<p>Execute a query to see the results</p>
|
||||
</div>
|
||||
|
||||
@@ -25,9 +25,6 @@ export const useQueryTabStyles = makeStyles({
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
"@media (max-width: 420px)": {
|
||||
overflow: "scroll",
|
||||
},
|
||||
},
|
||||
queryResultsMessage: {
|
||||
...shorthands.margin("5px"),
|
||||
@@ -41,9 +38,6 @@ export const useQueryTabStyles = makeStyles({
|
||||
display: "flex",
|
||||
rowGap: "12px",
|
||||
flexDirection: "column",
|
||||
"@media (max-width: 420px)": {
|
||||
height: "auto",
|
||||
},
|
||||
},
|
||||
queryResultsTabContentContainer: {
|
||||
display: "flex",
|
||||
@@ -99,12 +93,4 @@ export const useQueryTabStyles = makeStyles({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
},
|
||||
responsiveImg: {
|
||||
"@media (max-width: 420px)": {
|
||||
width: "50px",
|
||||
},
|
||||
},
|
||||
zoomedImageSize: {
|
||||
width: "60px",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ import { readTriggers } from "../../Common/dataAccess/readTriggers";
|
||||
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { BulkInsertResult, UploadDetailsRecord } 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";
|
||||
@@ -1092,13 +1092,17 @@ export default class Collection implements ViewModels.Collection {
|
||||
});
|
||||
}
|
||||
|
||||
public async bulkInsertDocuments(documents: JSONObject[]): Promise<BulkInsertResult> {
|
||||
const stats: BulkInsertResult = {
|
||||
public async bulkInsertDocuments(documents: JSONObject[]): Promise<{
|
||||
numSucceeded: number;
|
||||
numFailed: number;
|
||||
numThrottled: number;
|
||||
errors: string[];
|
||||
}> {
|
||||
const stats = {
|
||||
numSucceeded: 0,
|
||||
numFailed: 0,
|
||||
numThrottled: 0,
|
||||
errors: [] as string[],
|
||||
resources: [],
|
||||
};
|
||||
|
||||
const chunkSize = 100; // 100 is the max # of bulk operations the SDK currently accepts
|
||||
@@ -1116,12 +1120,8 @@ export default class Collection implements ViewModels.Collection {
|
||||
responses.forEach((response, index) => {
|
||||
if (response.statusCode === 201) {
|
||||
stats.numSucceeded++;
|
||||
stats.resources.push(response.resourceBody);
|
||||
} else if (response.statusCode === 429) {
|
||||
documentsToAttempt.push(attemptedDocuments[index]);
|
||||
} else if (response.statusCode === 409) {
|
||||
stats.numFailed++;
|
||||
stats.errors.push(`Document with id ${attemptedDocuments[index].id} already exists.`);
|
||||
} else {
|
||||
stats.numFailed++;
|
||||
stats.errors.push(JSON.stringify(response.resourceBody));
|
||||
@@ -1149,22 +1149,18 @@ export default class Collection implements ViewModels.Collection {
|
||||
numFailed: 0,
|
||||
numThrottled: 0,
|
||||
errors: [],
|
||||
resources: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const parsedContent = JSON.parse(documentContent);
|
||||
if (Array.isArray(parsedContent)) {
|
||||
const { numSucceeded, numFailed, numThrottled, errors, resources } =
|
||||
await this.bulkInsertDocuments(parsedContent);
|
||||
const { numSucceeded, numFailed, numThrottled, errors } = await this.bulkInsertDocuments(parsedContent);
|
||||
record.numSucceeded = numSucceeded;
|
||||
record.numFailed = numFailed;
|
||||
record.numThrottled = numThrottled;
|
||||
record.errors = errors;
|
||||
record.resources = record.resources.concat(resources);
|
||||
} else {
|
||||
const resource = await createDocument(this, parsedContent);
|
||||
record.resources.push(resource);
|
||||
await createDocument(this, parsedContent);
|
||||
record.numSucceeded++;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,15 @@ export const checkDatabaseResourceTokensValidity = (tokenTimestamp: number): voi
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the connection tab in the settings page of the Fabric UX extension
|
||||
*/
|
||||
export const openSettingsConnectionTab = (): void => {
|
||||
if (configContext.platform === Platform.Fabric) {
|
||||
sendCachedDataMessage(FabricMessageTypes.OpenSettings, [{ settingsId: "Connection" }]);
|
||||
}
|
||||
};
|
||||
|
||||
export const isFabric = (): boolean => configContext.platform === Platform.Fabric;
|
||||
export const isFabricMirroredKey = (): boolean =>
|
||||
isFabric() && userContext.fabricContext?.artifactType === CosmosDbArtifactType.MIRRORED_KEY;
|
||||
|
||||
@@ -149,7 +149,6 @@ export enum Action {
|
||||
UploadDocuments, // Used in Fabric. Please do not rename.
|
||||
CloudShellUserConsent,
|
||||
CloudShellTerminalSession,
|
||||
OpenVSCode,
|
||||
}
|
||||
|
||||
export const ActionModifiers = {
|
||||
|
||||
@@ -117,7 +117,6 @@ export interface UserContext {
|
||||
readonly feedbackPolicies?: AdminFeedbackPolicySettings;
|
||||
readonly dataPlaneRbacEnabled?: boolean;
|
||||
readonly refreshCosmosClient?: boolean;
|
||||
throughputBucketsEnabled?: boolean;
|
||||
}
|
||||
|
||||
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { configContext } from "ConfigContext";
|
||||
import { FeatureRegistration } from "Contracts/DataModels";
|
||||
import { AuthorizationTokenHeaderMetadata } from "Contracts/ViewModels";
|
||||
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
|
||||
|
||||
export const featureRegistered = async (subscriptionId: string, feature: string) => {
|
||||
const api_version = "2021-07-01";
|
||||
const url = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.Features/featureProviders/Microsoft.DocumentDB/subscriptionFeatureRegistrations/${feature}?api-version=${api_version}`;
|
||||
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await _fetchWithTimeout(url, headers);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!response?.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const featureRegistration = (await response?.json()) as FeatureRegistration;
|
||||
return featureRegistration?.properties?.state === "Registered";
|
||||
};
|
||||
|
||||
async function _fetchWithTimeout(
|
||||
url: string,
|
||||
headers: {
|
||||
[x: string]: string;
|
||||
},
|
||||
) {
|
||||
const timeout = 10000;
|
||||
const options = { timeout };
|
||||
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
const response = await window.fetch(url, {
|
||||
headers,
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(id);
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -245,7 +245,7 @@ export function downloadItem(
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
container.getDownloadModalContent(name),
|
||||
container.getDownloadModalConent(name),
|
||||
);
|
||||
}
|
||||
export async function downloadNotebookItem(
|
||||
|
||||
@@ -124,7 +124,7 @@ export const extractPartitionKeyValues = (
|
||||
documentContent: any,
|
||||
partitionKeyDefinition: PartitionKeyDefinition,
|
||||
): PartitionKey[] => {
|
||||
if (!partitionKeyDefinition.paths || partitionKeyDefinition.paths.length === 0) {
|
||||
if (!partitionKeyDefinition.paths || partitionKeyDefinition.paths.length === 0 || partitionKeyDefinition.systemKey) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ export const extractPartitionKeyValues = (
|
||||
|
||||
if (value !== undefined) {
|
||||
partitionKeyValues.push(value);
|
||||
} else if (!partitionKeyDefinition.systemKey) {
|
||||
} else {
|
||||
partitionKeyValues.push({});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -21,11 +21,3 @@ export function copyStyles(sourceDoc: Document, targetDoc: Document): void {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally returns a class name based on a boolean condition.
|
||||
* If the condition is true, returns the `trueValue` class; otherwise, returns `falseValue` (or an empty string if not provided).
|
||||
*/
|
||||
export function conditionalClass(condition: boolean, trueValue: string, falseValue?: string): string {
|
||||
return condition ? trueValue : falseValue || "";
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const useZoomLevel = (threshold: number = 2): boolean => {
|
||||
const [isZoomed, setIsZoomed] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkZoom = () => {
|
||||
const zoomLevel = window.devicePixelRatio;
|
||||
setIsZoomed(zoomLevel >= threshold);
|
||||
};
|
||||
|
||||
checkZoom();
|
||||
window.addEventListener("resize", checkZoom);
|
||||
return () => window.removeEventListener("resize", checkZoom);
|
||||
}, [threshold]);
|
||||
|
||||
return isZoomed;
|
||||
};
|
||||
|
||||
export default useZoomLevel;
|
||||
@@ -30,7 +30,7 @@
|
||||
<clear />
|
||||
<add name="X-Xss-Protection" value="1; mode=block" />
|
||||
<add name="X-Content-Type-Options" value="nosniff" />
|
||||
<add name="Content-Security-Policy" value="frame-src 'vscode:' frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net *.fabric.microsoft.com *.powerbi.com *.analysis-df.windows.net dataexplorer-preview.azurewebsites.net" />
|
||||
<add name="Content-Security-Policy" value="frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net *.fabric.microsoft.com *.powerbi.com *.analysis-df.windows.net dataexplorer-preview.azurewebsites.net" />
|
||||
</customHeaders>
|
||||
<redirectHeaders>
|
||||
<clear />
|
||||
|
||||
Reference in New Issue
Block a user