Compare commits

..

6 Commits

Author SHA1 Message Date
JustinKol
d2294593d9 Missing vs code button and image (#2155)
* Reverting back to popup by default as too many factors are present using a timeout

* Added telemetry constant for VSCode

* VSCode image and button
2025-05-15 17:55:55 -04:00
Nishtha Ahuja
3bcf3a4af5 Connect with VScode on Quickstart Page (#2153)
Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
2025-05-15 19:45:55 +05:30
asier-isayas
03890b8a4c Default New Global Secondary Index Panel to be sharded (#2138) (#2152)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-05-15 10:03:16 -04:00
sunghyunkang1111
6be46bff28 Throughput bucketing build release (#2149)
* Added AFEC check in userContext (#2140)

* Added AFEC check in userContext

* Update unit tests and fix Group to Bucket

* Hide throughput bucket settings for nonshared, nondedicated throughput container (#2146)
2025-05-14 12:26:30 -05:00
JustinKol
831c17726f Adding Opening VS Code via Data Explorer (#2150)
* Reverting back to popup by default as too many factors are present using a timeout

* Added telemetry constant for VSCode

* Reverting back to popup by default as too many factors are present using a timeout
2025-05-14 10:40:32 -04:00
Nishtha Ahuja
3cc26df6a1 bug fix for manual pricing (#2144)
Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
2025-05-13 20:52:11 +05:30
28 changed files with 137 additions and 439 deletions

8
images/VisualStudio.svg Normal file

File diff suppressed because one or more lines are too long

View File

@@ -187,8 +187,7 @@ if (process.env.NODE_ENV === "development") {
PROXY_PATH: "/proxy", PROXY_PATH: "/proxy",
EMULATOR_ENDPOINT: "https://localhost:8081", EMULATOR_ENDPOINT: "https://localhost:8081",
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac, PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
// MONGO_PROXY_ENDPOINT: "https://cosmos-db-portal-mongoproxy1-mpac-westus.azurewebsites.net", MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
MONGO_PROXY_ENDPOINT: "https://localhost:7238",
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Mpac, CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Mpac,
}); });
} }

View File

@@ -1,7 +1,6 @@
import { AuthType } from "AuthType"; import { AuthType } from "AuthType";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import ko from "knockout"; import ko from "knockout";
import { Features } from "Platform/Hosted/extractFeatures";
import React from "react"; import React from "react";
import { updateCollection } from "../../../Common/dataAccess/updateCollection"; import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer";
@@ -253,7 +252,7 @@ describe("SettingsComponent", () => {
it("should save throughput bucket changes when Save button is clicked", async () => { it("should save throughput bucket changes when Save button is clicked", async () => {
updateUserContext({ updateUserContext({
apiType: "SQL", apiType: "SQL",
features: { enableThroughputBuckets: true } as Features, throughputBucketsEnabled: true,
authType: AuthType.AAD, authType: AuthType.AAD,
}); });

View File

@@ -191,10 +191,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection); this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy; this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
this.throughputBucketsEnabled = this.throughputBucketsEnabled = userContext.throughputBucketsEnabled;
userContext.apiType === "SQL" &&
userContext.features.enableThroughputBuckets &&
userContext.authType === AuthType.AAD;
// Mongo container with system partition key still treat as "Fixed" // Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer = this.isFixedContainer =
@@ -1074,11 +1071,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
databaseId: this.collection.databaseId, databaseId: this.collection.databaseId,
collectionId: this.collection.id(), collectionId: this.collection.id(),
currentOffer: this.collection.offer(), currentOffer: this.collection.offer(),
autopilotThroughput: this.collection.offer().autoscaleMaxThroughput autopilotThroughput: this.collection.offer?.()?.autoscaleMaxThroughput
? this.collection.offer().autoscaleMaxThroughput ? this.collection.offer?.()?.autoscaleMaxThroughput
: undefined, : undefined,
manualThroughput: this.collection.offer().manualThroughput manualThroughput: this.collection.offer?.()?.manualThroughput
? this.collection.offer().manualThroughput ? this.collection.offer?.()?.manualThroughput
: undefined, : undefined,
throughputBuckets: this.state.throughputBuckets, throughputBuckets: this.state.throughputBuckets,
}); });
@@ -1344,7 +1341,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}); });
} }
if (this.throughputBucketsEnabled) { if (this.throughputBucketsEnabled && !hasDatabaseSharedThroughput(this.collection) && this.offer) {
tabs.push({ tabs.push({
tab: SettingsV2TabTypes.ThroughputBucketsTab, tab: SettingsV2TabTypes.ThroughputBucketsTab,
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />, content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,

View File

@@ -143,39 +143,4 @@ describe("SubSettingsComponent", () => {
expect(subSettingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn); expect(subSettingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn);
expect(subSettingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff); 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);
});
}); });

View File

@@ -63,16 +63,12 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
private geospatialVisible: boolean; private geospatialVisible: boolean;
private partitionKeyValue: string; private partitionKeyValue: string;
private partitionKeyName: string; private partitionKeyName: string;
private uniqueKeyName: string;
private uniqueKeyValue: string;
constructor(props: SubSettingsComponentProps) { constructor(props: SubSettingsComponentProps) {
super(props); super(props);
this.geospatialVisible = userContext.apiType === "SQL"; this.geospatialVisible = userContext.apiType === "SQL";
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key"; this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
this.partitionKeyValue = this.getPartitionKeyValue(); this.partitionKeyValue = this.getPartitionKeyValue();
this.uniqueKeyName = "Unique keys";
this.uniqueKeyValue = this.getUniqueKeyValue();
} }
componentDidMount(): void { componentDidMount(): void {
@@ -355,28 +351,6 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
public isLargePartitionKeyEnabled = (): boolean => this.props.collection.partitionKey?.version >= 2; public isLargePartitionKeyEnabled = (): boolean => this.props.collection.partitionKey?.version >= 2;
public isHierarchicalPartitionedContainer = (): boolean => this.props.collection.partitionKey?.kind === "MultiHash"; 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 { public render(): JSX.Element {
return ( return (
<Stack {...subComponentStackProps}> <Stack {...subComponentStackProps}>
@@ -389,8 +363,6 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
{this.props.changeFeedPolicyVisible && this.getChangeFeedComponent()} {this.props.changeFeedPolicyVisible && this.getChangeFeedComponent()}
{this.getPartitionKeyComponent()} {this.getPartitionKeyComponent()}
{this.getUniqueKeyComponent()}
</Stack> </Stack>
); );
} }

View File

@@ -26,7 +26,7 @@ describe("ThroughputBucketsComponent", () => {
it("renders the correct number of buckets", () => { it("renders the correct number of buckets", () => {
render(<ThroughputBucketsComponent {...defaultProps} />); render(<ThroughputBucketsComponent {...defaultProps} />);
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5); expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(5);
}); });
it("renders buckets in the correct order even if input is unordered", () => { it("renders buckets in the correct order even if input is unordered", () => {
@@ -36,8 +36,14 @@ describe("ThroughputBucketsComponent", () => {
]; ];
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={unorderedBuckets} />); render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={unorderedBuckets} />);
const bucketLabels = screen.getAllByText(/Group \d+/).map((el) => el.textContent); const bucketLabels = screen.getAllByText(/Bucket \d+/).map((el) => el.textContent);
expect(bucketLabels).toEqual(["Group 1 (Data Explorer Query Bucket)", "Group 2", "Group 3", "Group 4", "Group 5"]); expect(bucketLabels).toEqual([
"Bucket 1 (Data Explorer Query Bucket)",
"Bucket 2",
"Bucket 3",
"Bucket 4",
"Bucket 5",
]);
}); });
it("renders all provided buckets even if they exceed the max default bucket count", () => { it("renders all provided buckets even if they exceed the max default bucket count", () => {
@@ -53,7 +59,7 @@ describe("ThroughputBucketsComponent", () => {
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={oversizedBuckets} />); render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={oversizedBuckets} />);
expect(screen.getAllByText(/Group \d+/)).toHaveLength(7); expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(7);
expect(screen.getByDisplayValue("50")).toBeInTheDocument(); expect(screen.getByDisplayValue("50")).toBeInTheDocument();
expect(screen.getByDisplayValue("60")).toBeInTheDocument(); expect(screen.getByDisplayValue("60")).toBeInTheDocument();
@@ -171,7 +177,7 @@ describe("ThroughputBucketsComponent", () => {
it("ensures default buckets are used when no buckets are provided", () => { it("ensures default buckets are used when no buckets are provided", () => {
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={[]} />); render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={[]} />);
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5); expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(5);
expect(screen.getAllByDisplayValue("100")).toHaveLength(5); expect(screen.getAllByDisplayValue("100")).toHaveLength(5);
}); });
}); });

View File

@@ -76,7 +76,7 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
value={bucket.maxThroughputPercentage} value={bucket.maxThroughputPercentage}
onChange={(newValue) => handleBucketChange(bucket.id, newValue)} onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
showValue={false} showValue={false}
label={`Group ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`} label={`Bucket ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
styles={{ root: { flex: 2, maxWidth: 400 } }} styles={{ root: { flex: 2, maxWidth: 400 } }}
disabled={bucket.maxThroughputPercentage === 100} disabled={bucket.maxThroughputPercentage === 100}
/> />

View File

@@ -285,7 +285,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
serverId, serverId,
numberOfRegions, numberOfRegions,
isMultimaster, isMultimaster,
true, false,
); );
return ( return (
<div> <div>

View File

@@ -231,34 +231,6 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
Non-hierarchically partitioned container. Non-hierarchically partitioned container.
</Text> </Text>
</Stack> </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> </Stack>
`; `;
@@ -548,34 +520,6 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
Non-hierarchically partitioned container. Non-hierarchically partitioned container.
</Text> </Text>
</Stack> </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> </Stack>
`; `;
@@ -825,34 +769,6 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
Non-hierarchically partitioned container. Non-hierarchically partitioned container.
</Text> </Text>
</Stack> </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> </Stack>
`; `;
@@ -1167,34 +1083,6 @@ exports[`SubSettingsComponent renders 1`] = `
Non-hierarchically partitioned container. Non-hierarchically partitioned container.
</Text> </Text>
</Stack> </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> </Stack>
`; `;
@@ -1483,33 +1371,5 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
Non-hierarchically partitioned container. Non-hierarchically partitioned container.
</Text> </Text>
</Stack> </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> </Stack>
`; `;

View File

@@ -17,15 +17,7 @@ export const collection = {
includedPaths: [], includedPaths: [],
excludedPaths: [], excludedPaths: [],
}), }),
rawDataModel: { uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
uniqueKeyPolicy: {
uniqueKeys: [
{
paths: ["/id"],
},
],
},
},
usageSizeInKB: ko.observable(100), usageSizeInKB: ko.observable(100),
offer: ko.observable<DataModels.Offer>({ offer: ko.observable<DataModels.Offer>({
autoscaleMaxThroughput: undefined, autoscaleMaxThroughput: undefined,

View File

@@ -71,18 +71,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [ "partitionKeyProperties": [
"partitionKey", "partitionKey",
], ],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function], "readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function], "usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function], "vectorEmbeddingPolicy": [Function],
} }
@@ -163,18 +153,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [ "partitionKeyProperties": [
"partitionKey", "partitionKey",
], ],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function], "readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function], "usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function], "vectorEmbeddingPolicy": [Function],
} }
@@ -294,18 +274,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [ "partitionKeyProperties": [
"partitionKey", "partitionKey",
], ],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function], "readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function], "usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function], "vectorEmbeddingPolicy": [Function],
} }
@@ -434,18 +404,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyProperties": [ "partitionKeyProperties": [
"partitionKey", "partitionKey",
], ],
"rawDataModel": {
"uniqueKeyPolicy": {
"uniqueKeys": [
{
"paths": [
"/id",
],
},
],
},
},
"readSettings": [Function], "readSettings": [Function],
"uniqueKeyPolicy": {},
"usageSizeInKB": [Function], "usageSizeInKB": [Function],
"vectorEmbeddingPolicy": [Function], "vectorEmbeddingPolicy": [Function],
} }

View File

@@ -12,6 +12,7 @@ import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } fro
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils"; import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
import { featureRegistered } from "Utils/FeatureRegistrationUtils";
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts"; import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as ko from "knockout"; import * as ko from "knockout";
@@ -283,43 +284,12 @@ export default class Explorer {
} }
public openInVsCode(): void { public openInVsCode(): void {
TelemetryProcessor.traceStart(Action.OpenVSCode);
this.openVsCodeButtonClick();
}
private openVsCodeButtonClick(): void {
const activeTab = useTabs.getState().activeTab; const activeTab = useTabs.getState().activeTab;
const resourceId = encodeURIComponent(userContext.databaseAccount.id); const resourceId = encodeURIComponent(userContext.databaseAccount.id);
const database = encodeURIComponent(activeTab?.collection?.databaseId); const database = encodeURIComponent(activeTab?.collection?.databaseId);
const container = encodeURIComponent(activeTab?.collection?.id()); const container = encodeURIComponent(activeTab?.collection?.id());
const baseUrl = `vscod://ms-azuretools.vscode-cosmosdb?resourceId=${resourceId}`; const baseUrl = `vscode://ms-azuretools.vscode-cosmosdb?resourceId=${resourceId}`;
const vscodeUrl = activeTab ? `${baseUrl}&database=${database}&container=${container}` : baseUrl; const vscodeUrl = activeTab ? `${baseUrl}&database=${database}&container=${container}` : baseUrl;
const startTime = Date.now();
let vsCodeNotOpened = false;
setTimeout(() => {
const timeOutTime = Date.now() - startTime;
if (!vsCodeNotOpened && timeOutTime < 1050) {
vsCodeNotOpened = true;
useDialog.getState().openDialog(openVSCodeDialogProps);
}
}, 1000);
const link = document.createElement("a");
link.href = vscodeUrl;
link.rel = "noopener noreferrer";
document.body.appendChild(link);
try {
link.click();
document.body.removeChild(link);
TelemetryProcessor.traceStart(Action.OpenVSCode);
} catch (error) {
if (!vsCodeNotOpened) {
vsCodeNotOpened = true;
logConsoleError(`Failed to open VS Code: ${getErrorMessage(error)}`);
}
}
const openVSCodeDialogProps: DialogProps = { const openVSCodeDialogProps: DialogProps = {
linkProps: { linkProps: {
@@ -334,15 +304,19 @@ export default class Explorer {
secondaryButtonText: "Cancel", secondaryButtonText: "Cancel",
onPrimaryButtonClick: () => { onPrimaryButtonClick: () => {
vsCodeNotOpened = false; try {
this.openVsCodeButtonClick(); window.location.href = vscodeUrl;
useDialog.getState().closeDialog(); TelemetryProcessor.traceStart(Action.OpenVSCode);
} catch (error) {
logConsoleError(`Failed to open VS Code: ${getErrorMessage(error)}`);
}
}, },
onSecondaryButtonClick: () => { onSecondaryButtonClick: () => {
useDialog.getState().closeDialog(); useDialog.getState().closeDialog();
TelemetryProcessor.traceCancel(Action.OpenVSCode); TelemetryProcessor.traceCancel(Action.OpenVSCode);
}, },
}; };
useDialog.getState().openDialog(openVSCodeDialogProps);
} }
public async openCESCVAFeedbackBlade(): Promise<void> { public async openCESCVAFeedbackBlade(): Promise<void> {
@@ -1196,6 +1170,11 @@ export default class Explorer {
await this.initNotebooks(userContext.databaseAccount); await this.initNotebooks(userContext.databaseAccount);
} }
if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL") {
const throughputBucketsEnabled = await featureRegistered(userContext.subscriptionId, "ThroughputBucketing");
updateUserContext({ throughputBucketsEnabled });
}
this.refreshSampleData(); this.refreshSampleData();
} }

View File

@@ -517,6 +517,7 @@ export function createPostgreButtons(container: Explorer): CommandButtonComponen
export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] { export function createVCoreMongoButtons(container: Explorer): CommandButtonComponentProps[] {
const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo); const openVCoreMongoTerminalButton = createOpenTerminalButtonByKind(container, ViewModels.TerminalKind.VCoreMongo);
const addVsCode = createOpenVsCodeDialogButton(container); const addVsCode = createOpenVsCodeDialogButton(container);
return [openVCoreMongoTerminalButton, addVsCode]; return [openVCoreMongoTerminalButton, addVsCode];
} }

View File

@@ -608,7 +608,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<RightPaneForm {...genericPaneProps}> <RightPaneForm {...genericPaneProps}>
<div className={`paneMainContent ${styles.container}`}> <div className={`paneMainContent ${styles.container}`}>
{!isFabricNative() && ( {!isFabricNative() && (
<Accordion className={`customAccordion ${styles.firstItem}`} collapsible> <Accordion className={`customAccordion ${styles.firstItem}`}>
{shouldShowQueryPageOptions && ( {shouldShowQueryPageOptions && (
<AccordionItem value="1"> <AccordionItem value="1">
<AccordionHeader> <AccordionHeader>

View File

@@ -12,7 +12,6 @@ exports[`Settings Pane should render Default properly 1`] = `
> >
<Accordion <Accordion
className="customAccordion ___1uf6361_0000000 fz7g6wx" className="customAccordion ___1uf6361_0000000 fz7g6wx"
collapsible={true}
> >
<AccordionItem <AccordionItem
value="1" value="1"
@@ -574,7 +573,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
> >
<Accordion <Accordion
className="customAccordion ___1uf6361_0000000 fz7g6wx" className="customAccordion ___1uf6361_0000000 fz7g6wx"
collapsible={true}
> >
<AccordionItem <AccordionItem
value="7" value="7"

View File

@@ -356,7 +356,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
value="" value=""
> >
<div <div
className="ms-TextField is-required root-116" className="ms-TextField is-required root-110"
> >
<div <div
className="ms-TextField-wrapper" className="ms-TextField-wrapper"
@@ -647,7 +647,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
} }
> >
<label <label
className="ms-Label root-127" className="ms-Label root-121"
htmlFor="TextField0" htmlFor="TextField0"
id="TextFieldLabel2" id="TextFieldLabel2"
> >
@@ -656,13 +656,13 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
</LabelBase> </LabelBase>
</StyledLabelBase> </StyledLabelBase>
<div <div
className="ms-TextField-fieldGroup fieldGroup-117" className="ms-TextField-fieldGroup fieldGroup-111"
> >
<input <input
aria-invalid={false} aria-invalid={false}
aria-labelledby="TextFieldLabel2" aria-labelledby="TextFieldLabel2"
autoFocus={true} autoFocus={true}
className="ms-TextField-field field-118" className="ms-TextField-field field-112"
id="TextField0" id="TextField0"
name="collectionIdConfirmation" name="collectionIdConfirmation"
onBlur={[Function]} onBlur={[Function]}
@@ -2464,7 +2464,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<button <button
aria-label="Create" aria-label="Create"
className="ms-Button ms-Button--primary root-128" className="ms-Button ms-Button--primary root-122"
data-is-focusable={true} data-is-focusable={true}
data-test="Panel/OkButton" data-test="Panel/OkButton"
id="sidePanelOkButton" id="sidePanelOkButton"
@@ -2477,14 +2477,14 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
type="submit" type="submit"
> >
<span <span
className="ms-Button-flexContainer flexContainer-129" className="ms-Button-flexContainer flexContainer-123"
data-automationid="splitbuttonprimary" data-automationid="splitbuttonprimary"
> >
<span <span
className="ms-Button-textContainer textContainer-130" className="ms-Button-textContainer textContainer-124"
> >
<span <span
className="ms-Button-label label-132" className="ms-Button-label label-126"
id="id__5" id="id__5"
key="id__5" key="id__5"
> >

View File

@@ -2,13 +2,9 @@ import {
DetailsList, DetailsList,
DetailsListLayoutMode, DetailsListLayoutMode,
DirectionalHint, DirectionalHint,
FontIcon,
IColumn, IColumn,
SelectionMode, SelectionMode,
TooltipHost, TooltipHost,
getTheme,
mergeStyles,
mergeStyleSets,
} from "@fluentui/react"; } from "@fluentui/react";
import { Upload } from "Common/Upload/Upload"; import { Upload } from "Common/Upload/Upload";
import { UploadDetailsRecord } from "Contracts/ViewModels"; import { UploadDetailsRecord } from "Contracts/ViewModels";
@@ -18,36 +14,6 @@ import { getErrorMessage } from "../../Tables/Utilities";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; 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 const UploadItemsPane: FunctionComponent = () => { export const UploadItemsPane: FunctionComponent = () => {
const [files, setFiles] = useState<FileList>(); const [files, setFiles] = useState<FileList>();
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]); const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
@@ -94,94 +60,44 @@ export const UploadItemsPane: FunctionComponent = () => {
}; };
const columns: IColumn[] = [ 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", key: "fileName",
name: "FILE NAME", name: "FILE NAME",
fieldName: "fileName", fieldName: "fileName",
minWidth: 120, minWidth: 140,
maxWidth: 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", key: "status",
name: "STATUS", name: "STATUS",
fieldName: "numSucceeded", fieldName: "numSucceeded",
minWidth: 120, minWidth: 140,
maxWidth: 140, maxWidth: 140,
isRowHeader: true, isRowHeader: true,
isResizable: true, isResizable: true,
data: "string", data: "string",
isPadded: true, 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 ( return (
<RightPaneForm {...props}> <RightPaneForm {...props}>
<div className="paneMainContent"> <div className="paneMainContent">
@@ -199,6 +115,7 @@ export const UploadItemsPane: FunctionComponent = () => {
<DetailsList <DetailsList
items={uploadFileData} items={uploadFileData}
columns={columns} columns={columns}
onRenderItemColumn={_renderItemColumn}
selectionMode={SelectionMode.none} selectionMode={SelectionMode.none}
layoutMode={DetailsListLayoutMode.justified} layoutMode={DetailsListLayoutMode.justified}
isHeaderVisible={true} isHeaderVisible={true}

View File

@@ -28,6 +28,7 @@ import LinkIcon from "../../../images/Link_blue.svg";
import PowerShellIcon from "../../../images/PowerShell.svg"; import PowerShellIcon from "../../../images/PowerShell.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg"; import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg"; import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
import VisualStudioIcon from "../../../images/VisualStudio.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import CollectionIcon from "../../../images/tree-collection.svg"; import CollectionIcon from "../../../images/tree-collection.svg";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
@@ -458,10 +459,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
} }
if (userContext.apiType === "VCoreMongo") { if (userContext.apiType === "VCoreMongo") {
icon = ContainersIcon; icon = VisualStudioIcon;
title = "Connect with Studio 3T"; title = "Connect with VS Code";
description = "Prefer Studio 3T? Find your connection strings here"; description = "Query and Manage your MongoDB cluster in Visual Studio Code";
onClick = () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect); onClick = () => this.container.openInVsCode();
} }
return { return {

View File

@@ -729,7 +729,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
} else if (result.statusCode >= 400) { } else if (result.statusCode >= 400) {
newFailed.push(result.documentId); newFailed.push(result.documentId);
logConsoleError( 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}`,
); );
} }
}); });

View File

@@ -128,7 +128,7 @@ export default class MongoShellTabComponent extends Component<
apiEndpoint: apiEndpoint, apiEndpoint: apiEndpoint,
}, },
}, },
"https://localhost:443", window.origin,
); );
} }

View File

@@ -7,5 +7,5 @@ export function getMongoShellUrl(): string {
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint; const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`; const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
return `https://localhost:443/index.html?${queryString}`; return `/mongoshell/index.html?${queryString}`;
} }

View File

@@ -1122,9 +1122,6 @@ export default class Collection implements ViewModels.Collection {
stats.numSucceeded++; stats.numSucceeded++;
} else if (response.statusCode === 429) { } else if (response.statusCode === 429) {
documentsToAttempt.push(attemptedDocuments[index]); documentsToAttempt.push(attemptedDocuments[index]);
} else if (response.statusCode === 409) {
stats.numFailed++;
stats.errors.push(`Document with id ${attemptedDocuments[index].id} already exists.`);
} else { } else {
stats.numFailed++; stats.numFailed++;
stats.errors.push(JSON.stringify(response.resourceBody)); stats.errors.push(JSON.stringify(response.resourceBody));

View File

@@ -117,6 +117,7 @@ export interface UserContext {
readonly feedbackPolicies?: AdminFeedbackPolicySettings; readonly feedbackPolicies?: AdminFeedbackPolicySettings;
readonly dataPlaneRbacEnabled?: boolean; readonly dataPlaneRbacEnabled?: boolean;
readonly refreshCosmosClient?: boolean; readonly refreshCosmosClient?: boolean;
throughputBucketsEnabled?: boolean;
} }
export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo"; export type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra" | "Postgres" | "VCoreMongo";

View File

@@ -5,7 +5,6 @@ export function validateEndpoint(
endpointToValidate: string | undefined, endpointToValidate: string | undefined,
allowedEndpoints: ReadonlyArray<string>, allowedEndpoints: ReadonlyArray<string>,
): boolean { ): boolean {
return true;
try { try {
return validateEndpointInternal( return validateEndpointInternal(
endpointToValidate, endpointToValidate,

View File

@@ -0,0 +1,48 @@
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;
}

View File

@@ -5,7 +5,6 @@ export function isInvalidParentFrameOrigin(event: MessageEvent): boolean {
} }
function isValidOrigin(allowedOrigins: ReadonlyArray<string>, event: MessageEvent): boolean { function isValidOrigin(allowedOrigins: ReadonlyArray<string>, event: MessageEvent): boolean {
return true;
const eventOrigin = (event && event.origin) || ""; const eventOrigin = (event && event.origin) || "";
const windowOrigin = (window && window.origin) || ""; const windowOrigin = (window && window.origin) || "";
if (eventOrigin === windowOrigin) { if (eventOrigin === windowOrigin) {

View File

@@ -30,7 +30,7 @@
<clear /> <clear />
<add name="X-Xss-Protection" value="1; mode=block" /> <add name="X-Xss-Protection" value="1; mode=block" />
<add name="X-Content-Type-Options" value="nosniff" /> <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> </customHeaders>
<redirectHeaders> <redirectHeaders>
<clear /> <clear />