From 0f6c979268ecbdcea243891c1eab0df1c70ca4be Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Thu, 3 Apr 2025 11:10:40 -0500 Subject: [PATCH 01/87] Update cleanupDBs.js (#2093) --- utils/cleanupDBs.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/utils/cleanupDBs.js b/utils/cleanupDBs.js index 3c89a8c5c..723ec1c73 100644 --- a/utils/cleanupDBs.js +++ b/utils/cleanupDBs.js @@ -20,6 +20,10 @@ async function main() { const client = new CosmosDBManagementClient(credentials, subscriptionId); const accounts = await client.databaseAccounts.list(resourceGroupName); for (const account of accounts) { + if (account.name.endsWith("-readonly")) { + console.log(`SKIPPED: ${account.name}`); + continue; + } if (account.kind === "MongoDB") { const mongoDatabases = await client.mongoDBResources.listMongoDBDatabases(resourceGroupName, account.name); for (const database of mongoDatabases) { From e3c3a8b1b792a8ba69d480540e318ad4666c3818 Mon Sep 17 00:00:00 2001 From: bogercraig <124094535+bogercraig@users.noreply.github.com> Date: Thu, 3 Apr 2025 17:19:28 -0700 Subject: [PATCH 02/87] Hide Keys and Connection Strings in Connect Tab (#2095) * Hide connection strings and keys by default. Move URI above pivot since common across tabs. Matches frontend. Need to add scrolling of keys when window is small. Possibly reduce URI width. * Add vertical scrolling when window size reduces. * Adding missing semicolon at end of connection strings. --- src/Explorer/Tabs/ConnectTab.tsx | 143 ++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 40 deletions(-) diff --git a/src/Explorer/Tabs/ConnectTab.tsx b/src/Explorer/Tabs/ConnectTab.tsx index 3c334fbc1..62f190996 100644 --- a/src/Explorer/Tabs/ConnectTab.tsx +++ b/src/Explorer/Tabs/ConnectTab.tsx @@ -16,10 +16,20 @@ export const ConnectTab: React.FC = (): JSX.Element => { const [primaryReadonlyMasterKey, setPrimaryReadonlyMasterKey] = useState(""); const [secondaryReadonlyMasterKey, setSecondaryReadonlyMasterKey] = useState(""); const uri: string = userContext.databaseAccount.properties?.documentEndpoint; - const primaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryMasterKey}`; - const secondaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryMasterKey}`; - const primaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryReadonlyMasterKey}`; - const secondaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryReadonlyMasterKey}`; + const primaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryMasterKey};`; + const secondaryConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryMasterKey};`; + const primaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${primaryReadonlyMasterKey};`; + const secondaryReadonlyConnectionStr = `AccountEndpoint=${uri};AccountKey=${secondaryReadonlyMasterKey};`; + const maskedValue: string = + "*********************************************************************************************************************************"; + const [showPrimaryMasterKey, setShowPrimaryMasterKey] = useState(false); + const [showSecondaryMasterKey, setShowSecondaryMasterKey] = useState(false); + const [showPrimaryReadonlyMasterKey, setShowPrimaryReadonlyMasterKey] = useState(false); + const [showSecondaryReadonlyMasterKey, setShowSecondaryReadonlyMasterKey] = useState(false); + const [showPrimaryConnectionStr, setShowPrimaryConnectionStr] = useState(false); + const [showSecondaryConnectionStr, setShowSecondaryConnectionStr] = useState(false); + const [showPrimaryReadonlyConnectionStr, setShowPrimaryReadonlyConnectionStr] = useState(false); + const [showSecondaryReadonlyConnectionStr, setShowSecondaryReadonlyConnectionStr] = useState(false); useEffect(() => { fetchKeys(); @@ -62,55 +72,97 @@ export const ConnectTab: React.FC = (): JSX.Element => { root: { width: "100%" }, field: { backgroundColor: "rgb(230, 230, 230)" }, fieldGroup: { borderColor: "rgb(138, 136, 134)" }, + suffix: { + backgroundColor: "rgb(230, 230, 230)", + margin: 0, + padding: 0, + }, }; + const renderCopyButton = (selector: string) => ( + onCopyBtnClicked(selector)} + styles={{ + root: { + height: "100%", + backgroundColor: "rgb(230, 230, 230)", + border: "none", + }, + rootHovered: { + backgroundColor: "rgb(220, 220, 220)", + }, + rootPressed: { + backgroundColor: "rgb(210, 210, 210)", + }, + }} + /> + ); + return (
+ + renderCopyButton("#uriTextfield")} + /> +
+
+ {userContext.hasWriteAccess && ( - - - - onCopyBtnClicked("#uriTextfield")} /> - - + renderCopyButton("#primaryKeyTextfield"), + })} + /> + setShowPrimaryMasterKey(!showPrimaryMasterKey)} /> - onCopyBtnClicked("#primaryKeyTextfield")} /> - renderCopyButton("#secondaryKeyTextfield"), + })} /> onCopyBtnClicked("#secondaryKeyTextfield")} + iconProps={{ iconName: showSecondaryMasterKey ? "Hide3" : "View" }} + onClick={() => setShowSecondaryMasterKey(!showSecondaryMasterKey)} /> - renderCopyButton("#primaryConStrTextfield"), + })} /> onCopyBtnClicked("#primaryConStrTextfield")} + iconProps={{ iconName: showPrimaryConnectionStr ? "Hide3" : "View" }} + onClick={() => setShowPrimaryConnectionStr(!showPrimaryConnectionStr)} /> @@ -118,34 +170,36 @@ export const ConnectTab: React.FC = (): JSX.Element => { label="SECONDARY CONNECTION STRING" id="secondaryConStrTextfield" readOnly - value={secondaryConnectionStr} + value={showSecondaryConnectionStr ? secondaryConnectionStr : maskedValue} styles={textfieldStyles} + {...(showSecondaryConnectionStr && { + onRenderSuffix: () => renderCopyButton("#secondaryConStrTextfield"), + })} /> onCopyBtnClicked("#secondaryConStrTextfield")} + iconProps={{ iconName: showSecondaryConnectionStr ? "Hide3" : "View" }} + onClick={() => setShowSecondaryConnectionStr(!showSecondaryConnectionStr)} /> )} - - - - onCopyBtnClicked("#uriReadOnlyTextfield")} /> - + renderCopyButton("#primaryReadonlyKeyTextfield"), + })} /> onCopyBtnClicked("#primaryReadonlyKeyTextfield")} + iconProps={{ iconName: showPrimaryReadonlyMasterKey ? "Hide3" : "View" }} + onClick={() => setShowPrimaryReadonlyMasterKey(!showPrimaryReadonlyMasterKey)} /> @@ -153,12 +207,15 @@ export const ConnectTab: React.FC = (): JSX.Element => { label="SECONDARY READ-ONLY KEY" id="secondaryReadonlyKeyTextfield" readOnly - value={secondaryReadonlyMasterKey} + value={showSecondaryReadonlyMasterKey ? secondaryReadonlyMasterKey : maskedValue} styles={textfieldStyles} + {...(showSecondaryReadonlyMasterKey && { + onRenderSuffix: () => renderCopyButton("#secondaryReadonlyKeyTextfield"), + })} /> onCopyBtnClicked("#secondaryReadonlyKeyTextfield")} + iconProps={{ iconName: showSecondaryReadonlyMasterKey ? "Hide3" : "View" }} + onClick={() => setShowSecondaryReadonlyMasterKey(!showSecondaryReadonlyMasterKey)} /> @@ -166,25 +223,31 @@ export const ConnectTab: React.FC = (): JSX.Element => { label="PRIMARY READ-ONLY CONNECTION STRING" id="primaryReadonlyConStrTextfield" readOnly - value={primaryReadonlyConnectionStr} + value={showPrimaryReadonlyConnectionStr ? primaryReadonlyConnectionStr : maskedValue} styles={textfieldStyles} + {...(showPrimaryReadonlyConnectionStr && { + onRenderSuffix: () => renderCopyButton("#primaryReadonlyConStrTextfield"), + })} /> onCopyBtnClicked("#primaryReadonlyConStrTextfield")} + iconProps={{ iconName: showPrimaryReadonlyConnectionStr ? "Hide3" : "View" }} + onClick={() => setShowPrimaryReadonlyConnectionStr(!showPrimaryReadonlyConnectionStr)} /> renderCopyButton("#secondaryReadonlyConStrTextfield"), + })} /> onCopyBtnClicked("#secondaryReadonlyConStrTextfield")} + iconProps={{ iconName: showSecondaryReadonlyConnectionStr ? "Hide3" : "View" }} + onClick={() => setShowSecondaryReadonlyConnectionStr(!showSecondaryReadonlyConnectionStr)} /> From af0a890516b6302b60710b4f54e58b68d88b1857 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Mon, 7 Apr 2025 10:45:29 -0500 Subject: [PATCH 03/87] Pk missing fix (#2094) * fix partition key missing not being able to load the document * Implement E2E tests for documents with different partitionkeys * Implement E2E tests for documents with different partitionkeys * Implement E2E tests for documents with different partitionkeys * Updated snapshot --- playwright.config.ts | 1 - .../Tabs/DocumentsTabV2/DocumentsTabV2.tsx | 17 +++- .../DocumentsTabV2.test.tsx.snap | 4 + src/Utils/QueryUtils.test.ts | 78 +++++++++++++--- src/Utils/QueryUtils.ts | 14 ++- test/fx.ts | 33 ++++++- test/sql/document.spec.ts | 45 ++++++++++ test/sql/testCases.ts | 88 +++++++++++++++++++ 8 files changed, 259 insertions(+), 21 deletions(-) create mode 100644 test/sql/document.spec.ts create mode 100644 test/sql/testCases.ts diff --git a/playwright.config.ts b/playwright.config.ts index 4c5ad3c14..5279a0660 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,4 @@ import { defineConfig, devices } from "@playwright/test"; - /** * See https://playwright.dev/docs/test-configuration. */ diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index c24ac08e8..5ef058f3f 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -1037,6 +1037,7 @@ export const DocumentsTabComponent: React.FunctionComponent { + // in case of any kind of failures of accidently changing partition key, restore the original + // so that when user navigates away from current document and comes back, + // it doesnt fail to load due to using the invalid partition keys + selectedDocumentId.partitionKeyValue = originalPartitionKeyValue; onExecutionErrorChange(true); const errorMessage = getErrorMessage(error); useDialog.getState().showOkModalDialog("Update document failed", errorMessage); @@ -2083,8 +2088,8 @@ export const DocumentsTabComponent: React.FunctionComponent -
-
+
+
{!isPreferredApiMongoDB && SELECT * FROM c } -
+
-
+
{isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
SELECT * FROM c @@ -65,6 +67,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = ` preferredSize="35%" >
{ version: 2, }; }; + const generatePartitionKeysForPaths = (paths: string[]): DataModels.PartitionKey => { + return { + paths: paths, + kind: "Hash", + version: 2, + }; + }; describe("buildDocumentsQueryPartitionProjections()", () => { it("should return empty string if partition key is undefined", () => { @@ -89,6 +96,18 @@ describe("Query Utils", () => { expect(query).toContain("c.id"); }); + + it("should always include {} for any missing partition keys", () => { + const query = QueryUtils.buildDocumentsQuery( + "", + ["a", "b", "c"], + generatePartitionKeysForPaths(["/a", "/b", "/c"]), + [], + ); + expect(query).toContain('IIF(IS_DEFINED(c["a"]), c["a"], {})'); + expect(query).toContain('IIF(IS_DEFINED(c["b"]), c["b"], {})'); + expect(query).toContain('IIF(IS_DEFINED(c["c"]), c["c"], {})'); + }); }); describe("queryPagesUntilContentPresent()", () => { @@ -201,18 +220,6 @@ describe("Query Utils", () => { expect(expectedPartitionKeyValues).toContain(documentContent["Category"]); }); - it("should extract no partition key values in the case nested partition key", () => { - const singlePartitionKeyDefinition: PartitionKeyDefinition = { - kind: PartitionKeyKind.Hash, - paths: ["/Location.type"], - }; - const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues( - documentContent, - singlePartitionKeyDefinition, - ); - expect(partitionKeyValues.length).toBe(0); - }); - it("should extract all partition key values for hierarchical and nested partition keys", () => { const mixedPartitionKeyDefinition: PartitionKeyDefinition = { kind: PartitionKeyKind.MultiHash, @@ -225,5 +232,52 @@ describe("Query Utils", () => { expect(partitionKeyValues.length).toBe(2); expect(partitionKeyValues).toEqual(["United States", "Point"]); }); + + it("if any partition key is null or empty string, the partitionKeyValues shall match", () => { + const newDocumentContent = { + ...documentContent, + ...{ + Country: null, + Location: { + type: "", + coordinates: [-121.49, 46.206], + }, + }, + }; + + const mixedPartitionKeyDefinition: PartitionKeyDefinition = { + kind: PartitionKeyKind.MultiHash, + paths: ["/Country", "/Location/type"], + }; + const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues( + newDocumentContent, + mixedPartitionKeyDefinition, + ); + expect(partitionKeyValues.length).toBe(2); + expect(partitionKeyValues).toEqual([null, ""]); + }); + + it("if any partition key doesn't exist, it should still set partitionkey value as {}", () => { + const newDocumentContent = { + ...documentContent, + ...{ + Country: null, + Location: { + coordinates: [-121.49, 46.206], + }, + }, + }; + + const mixedPartitionKeyDefinition: PartitionKeyDefinition = { + kind: PartitionKeyKind.MultiHash, + paths: ["/Country", "/Location/type"], + }; + const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues( + newDocumentContent, + mixedPartitionKeyDefinition, + ); + expect(partitionKeyValues.length).toBe(2); + expect(partitionKeyValues).toEqual([null, {}]); + }); }); }); diff --git a/src/Utils/QueryUtils.ts b/src/Utils/QueryUtils.ts index f0b39e4e2..07822a422 100644 --- a/src/Utils/QueryUtils.ts +++ b/src/Utils/QueryUtils.ts @@ -47,6 +47,7 @@ export function buildDocumentsQueryPartitionProjections( for (const index in partitionKey.paths) { // TODO: Handle "/" in partition key definitions const projectedProperties: string[] = partitionKey.paths[index].split("/").slice(1); + const isSystemPartitionKey: boolean = partitionKey.systemKey || false; let projectedProperty = ""; projectedProperties.forEach((property: string) => { @@ -61,8 +62,13 @@ export function buildDocumentsQueryPartitionProjections( projectedProperty += `[${projection}]`; } }); - - projections.push(`${collectionAlias}${projectedProperty}`); + const fullAccess = `${collectionAlias}${projectedProperty}`; + if (!isSystemPartitionKey) { + const wrappedProjection = `IIF(IS_DEFINED(${fullAccess}), ${fullAccess}, {})`; + projections.push(wrappedProjection); + } else { + projections.push(fullAccess); + } } return projections.join(","); @@ -118,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; } @@ -130,6 +136,8 @@ export const extractPartitionKeyValues = ( if (value !== undefined) { partitionKeyValues.push(value); + } else { + partitionKeyValues.push({}); } }); diff --git a/test/fx.ts b/test/fx.ts index 661d55897..14ad0acbb 100644 --- a/test/fx.ts +++ b/test/fx.ts @@ -26,7 +26,7 @@ export function getAzureCLICredentials(): AzureCliCredential { export async function getAzureCLICredentialsToken(): Promise { const credentials = getAzureCLICredentials(); - const token = (await credentials.getToken("https://management.core.windows.net//.default")).token; + const token = (await credentials.getToken("https://management.core.windows.net//.default"))?.token || ""; return token; } @@ -37,6 +37,7 @@ export enum TestAccount { Mongo = "Mongo", Mongo32 = "Mongo32", SQL = "SQL", + SQLReadOnly = "SQLReadOnly", } export const defaultAccounts: Record = { @@ -46,6 +47,7 @@ export const defaultAccounts: Record = { [TestAccount.Mongo]: "github-e2etests-mongo", [TestAccount.Mongo32]: "github-e2etests-mongo32", [TestAccount.SQL]: "github-e2etests-sql", + [TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly", }; export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests"; @@ -214,6 +216,25 @@ export class QueryTab { } } +export class DocumentsTab { + documentsFilter: Locator; + documentsListPane: Locator; + documentResultsPane: Locator; + resultsEditor: Editor; + + constructor( + public frame: Frame, + public tabId: string, + public tab: Locator, + public locator: Locator, + ) { + this.documentsFilter = this.locator.getByTestId("DocumentsTab/Filter"); + this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane"); + this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane"); + this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded")); + } +} + type PanelOpenOptions = { closeTimeout?: number; }; @@ -232,6 +253,12 @@ export class DataExplorer { return new QueryTab(this.frame, tabId, tab, queryTab); } + documentsTab(tabId: string): DocumentsTab { + const tab = this.tab(tabId); + const documentsTab = tab.getByTestId("DocumentsTab"); + return new DocumentsTab(this.frame, tabId, tab, documentsTab); + } + /** Select the primary global command button. * * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. @@ -294,6 +321,10 @@ export class DataExplorer { return await this.waitForNode(`${databaseId}/${containerId}`); } + async waitForContainerItemsNode(databaseId: string, containerId: string): Promise { + return await this.waitForNode(`${databaseId}/${containerId}/Items`); + } + /** Select the tree node with the specified id */ treeNode(id: string): TreeNode { return new TreeNode(this.frame.getByTestId(`TreeNode:${id}`), this.frame, id); diff --git a/test/sql/document.spec.ts b/test/sql/document.spec.ts new file mode 100644 index 000000000..10877702a --- /dev/null +++ b/test/sql/document.spec.ts @@ -0,0 +1,45 @@ +import { expect, test } from "@playwright/test"; + +import { DataExplorer, DocumentsTab, TestAccount } from "../fx"; +import { documentTestCases } from "./testCases"; + +let explorer: DataExplorer = null!; +let documentsTab: DocumentsTab = null!; + +for (const { name, databaseId, containerId, expectedDocumentIds } of documentTestCases) { + test.describe(`Test Documents with ${name}`, () => { + test.beforeEach("Open documents tab", async ({ page }) => { + explorer = await DataExplorer.open(page, TestAccount.SQLReadOnly); + + const containerNode = await explorer.waitForContainerNode(databaseId, containerId); + await containerNode.expand(); + + const containerMenuNode = await explorer.waitForContainerItemsNode(databaseId, containerId); + await containerMenuNode.element.click(); + + documentsTab = explorer.documentsTab("tab0"); + + await documentsTab.documentsFilter.waitFor(); + await documentsTab.documentsListPane.waitFor(); + await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 }); + }); + + for (const docId of expectedDocumentIds) { + test.describe(`Document ID: ${docId}`, () => { + test(`should load and view document ${docId}`, async () => { + const span = documentsTab.documentsListPane.getByText(docId, { exact: true }).nth(0); + await span.waitFor(); + await expect(span).toBeVisible(); + + await span.click(); + await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 }); + + const resultText = await documentsTab.resultsEditor.text(); + const resultData = JSON.parse(resultText!); + expect(resultText).not.toBeNull(); + expect(resultData?.id).toEqual(docId); + }); + }); + } + }); +} diff --git a/test/sql/testCases.ts b/test/sql/testCases.ts new file mode 100644 index 000000000..0f18bb183 --- /dev/null +++ b/test/sql/testCases.ts @@ -0,0 +1,88 @@ +type ContainerTestCase = { + name: string; + databaseId: string; + containerId: string; + expectedDocumentIds: string[]; +}; + +export const documentTestCases: ContainerTestCase[] = [ + { + name: "System Partition Key", + databaseId: "e2etests-sql-readonly", + containerId: "systemPartitionKey", + expectedDocumentIds: ["systempartition"], + }, + { + name: "Single Partition Key", + databaseId: "e2etests-sql-readonly", + containerId: "singlePartitionKey", + expectedDocumentIds: [ + "singlePartitionKey", + "singlePartitionKey_empty_string", + "singlePartitionKey_null", + "singlePartitionKey_missing", + ], + }, + { + name: "Single Nested Partition Key", + databaseId: "e2etests-sql-readonly", + containerId: "singleNestedPartitionKey", + expectedDocumentIds: [ + "singlePartitionKey_nested", + "singlePartitionKey_nested_empty_string", + "singlePartitionKey_nested_null", + "singlePartitionKey_nested_missing", + ], + }, + { + name: "2-Level Hierarchical Partition Key", + databaseId: "e2etests-sql-readonly", + containerId: "twoLevelPartitionKey", + expectedDocumentIds: [ + "twoLevelPartitionKey_value_empty", + "twoLevelPartitionKey_value_null", + "twoLevelPartitionKey_value_missing", + "twoLevelPartitionKey_empty_null", + "twoLevelPartitionKey_null_missing", + "twoLevelPartitionKey_missing_value", + ], + }, + { + name: "2-Level Hierarchical Nested Partition Key", + databaseId: "e2etests-sql-readonly", + containerId: "twoLevelNestedPartitionKey", + expectedDocumentIds: [ + "twoLevelNestedPartitionKey_nested_value_empty", + "twoLevelNestedPartitionKey_nested_null_missing", + "twoLevelNestedPartitionKey_nested_missing_value", + ], + }, + { + name: "3-Level Hierarchical Partition Key", + databaseId: "e2etests-sql-readonly", + containerId: "threeLevelPartitionKey", + expectedDocumentIds: [ + "threeLevelPartitionKey_value_empty_null", + "threeLevelPartitionKey_value_null_missing", + "threeLevelPartitionKey_value_missing_null", + "threeLevelPartitionKey_null_empty_value", + "threeLevelPartitionKey_missing_value_value", + "threeLevelPartitionKey_empty_value_missing", + ], + }, + { + name: "3-Level Nested Hierarchical Partition Key", + databaseId: "e2etests-sql-readonly", + containerId: "threeLevelNestedPartitionKey", + expectedDocumentIds: [ + "threeLevelNestedPartitionKey_nested_empty_value_null", + "threeLevelNestedPartitionKey_nested_null_value_missing", + "threeLevelNestedPartitionKey_nested_missing_value_null", + "threeLevelNestedPartitionKey_nested_null_empty_missing", + "threeLevelNestedPartitionKey_nested_value_missing_empty", + "threeLevelNestedPartitionKey_nested_missing_null_empty", + "threeLevelNestedPartitionKey_nested_empty_null_value", + "threeLevelNestedPartitionKey_nested_value_null_empty", + ], + }, +]; From 9bb1d0baceeb134725b08b695a4cc10d2113d541 Mon Sep 17 00:00:00 2001 From: bogercraig <124094535+bogercraig@users.noreply.github.com> Date: Mon, 7 Apr 2025 09:29:11 -0700 Subject: [PATCH 04/87] Manual Region Selection (#2037) * Add standin region selection to settings menu. * Retrieve read and write regions from user context and populate dropdown menu. Update local storage value. Need to now connect with updating read region of primary cosmos client. * Change to only selecting region for cosmos client. Not setting up separate read and write clients. * Add read and write endpoint logging to cosmos client. * Pass changing endpoint from settings menu to client. Encountered token issues using new endpoint in client. * Rough implementation of region selection of endpoint for cosmos client. Still need to: 1 - Use separate context var to track selected region. Directly updating database account context throws off token generation by acquireMSALTokenForAccount 2 - Remove href overrides in acquireMSALTokenForAccount. * Update region selection to include global endpoint and generate a unique list of read and write endpoints. Need to continue with clearing out selected endpoint when global is selected again. Write operations stall when read region is selected even though 403 returned when region rejects operation. Need to limit feature availablility to nosql, table, gremlin (maybe). * Update cosmos client to fix bug. Clients continuously generate after changing RBAC setting. * Swapping back to default endpoint value. * Rebase on client refresh bug fix. * Enable region selection for NoSql, Table, Gremlin * Add logic to reset regional endpoint when global is selected. * Fix state changing when selecting region or resetting to global. * Rough implementation of configuring regional endpoint when DE is loaded in portal or hosted with AAD/Entra auth. * Ininitial attempt at adding error handling, but still having issues with errors caught at proxy plugin. * Added rough error handling in local requestPlugin used in local environments. Passes new error to calling code. Might need to add specific error handling for request plugin to the handleError class. * Change how request plugin returns error so existing error handling utility can process and present error. * Only enable region selection for nosql accounts. * Limit region selection to portal and hosted AAD auth. SQL accounts only. Could possibly enable on table and gremlin later. * Update error handling to account for generic error code. * Refactor error code extraction. * Update test snapshots and remove unneeded logging. * Change error handling to use only the message rather than casting to any. * Clean up debug logging in cosmos client. * Remove unused storage keys. * Use endpoint instead of region name to track selected region. Prevents having to do endpoint lookups. * Add initial button state update depending on region selection. Need to update with the API and react to user context changes. * Disable CRUD buttons when read region selected. * Default to write enabled in react. * Disable query saving when read region is selected. * Patch clientWidth error on conflicts tab. * Resolve merge conflicts from rebase. * Make sure proxy endpoints return in all cases. * Remove excess client logging and match main for ConflictsTab. * Cleaning up logging and fixing endpoint discovery bug. * Fix formatting. * Reformatting if statements with preferred formatting. * Migrate region selection to local persistence. Fixes account swapping bug. TODO: Inspect better way to reset interface elements when deleteAllStates is called. Need to react to regional endpoint being reset. * Relocate resetting interface context to helper function. * Remove legacy state storage for regional endpoint selection. * Laurent suggestion updates. --- src/Common/CosmosClient.ts | 7 +- .../Panes/SettingsPane/SettingsPane.tsx | 168 ++++++++++++++++-- .../__snapshots__/SettingsPane.test.tsx.snap | 20 +-- .../Tabs/DocumentsTabV2/DocumentsTabV2.tsx | 13 +- .../Tabs/QueryTab/QueryTabComponent.tsx | 14 +- src/Shared/AppStatePersistenceUtility.ts | 1 + src/UserContext.ts | 2 + src/hooks/useClientWriteEnabled.ts | 10 ++ src/hooks/useKnockoutExplorer.ts | 46 +++++ 9 files changed, 256 insertions(+), 25 deletions(-) create mode 100644 src/hooks/useClientWriteEnabled.ts diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index cf34b2279..1ecd94944 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -125,7 +125,11 @@ export const endpoint = () => { const location = _global.parent ? _global.parent.location : _global.location; return configContext.EMULATOR_ENDPOINT || location.origin; } - return userContext.endpoint || userContext?.databaseAccount?.properties?.documentEndpoint; + return ( + userContext.selectedRegionalEndpoint || + userContext.endpoint || + userContext?.databaseAccount?.properties?.documentEndpoint + ); }; export async function getTokenFromAuthService( @@ -203,6 +207,7 @@ export function client(): Cosmos.CosmosClient { userAgentSuffix: "Azure Portal", defaultHeaders: _defaultHeaders, connectionPolicy: { + enableEndpointDiscovery: !userContext.selectedRegionalEndpoint, retryOptions: { maxRetryAttemptCount: LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts), fixedRetryIntervalInMilliseconds: LocalStorageUtility.getEntryNumber(StorageKey.RetryInterval), diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index 1626f09d1..a40f4da99 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -6,7 +6,9 @@ import { Checkbox, ChoiceGroup, DefaultButton, + Dropdown, IChoiceGroupOption, + IDropdownOption, ISpinButtonStyles, IToggleStyles, Position, @@ -21,7 +23,15 @@ import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { Platform, configContext } from "ConfigContext"; import { useDialog } from "Explorer/Controls/Dialog"; import { useDatabases } from "Explorer/useDatabases"; -import { deleteAllStates } from "Shared/AppStatePersistenceUtility"; +import { isFabric } from "Platform/Fabric/FabricUtil"; +import { + AppStateComponentNames, + deleteAllStates, + deleteState, + hasState, + loadState, + saveState, +} from "Shared/AppStatePersistenceUtility"; import { DefaultRUThreshold, LocalStorageUtility, @@ -37,6 +47,7 @@ import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { logConsoleError, logConsoleInfo } from "Utils/NotificationConsoleUtils"; import * as PriorityBasedExecutionUtils from "Utils/PriorityBasedExecutionUtils"; import { getReadOnlyKeys, listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts"; +import { useClientWriteEnabled } from "hooks/useClientWriteEnabled"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useSidePanel } from "hooks/useSidePanel"; import React, { FunctionComponent, useState } from "react"; @@ -143,6 +154,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ ? LocalStorageUtility.getEntryString(StorageKey.IsGraphAutoVizDisabled) : "false", ); + const [selectedRegionalEndpoint, setSelectedRegionalEndpoint] = useState( + hasState({ + componentName: AppStateComponentNames.SelectedRegionalEndpoint, + globalAccountName: userContext.databaseAccount?.name, + }) + ? (loadState({ + componentName: AppStateComponentNames.SelectedRegionalEndpoint, + globalAccountName: userContext.databaseAccount?.name, + }) as string) + : undefined, + ); const [retryAttempts, setRetryAttempts] = useState( LocalStorageUtility.hasItem(StorageKey.RetryAttempts) ? LocalStorageUtility.getEntryNumber(StorageKey.RetryAttempts) @@ -189,6 +211,44 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ configContext.platform !== Platform.Fabric && !isEmulator; const shouldShowPriorityLevelOption = PriorityBasedExecutionUtils.isFeatureEnabled() && !isEmulator; + + const uniqueAccountRegions = new Set(); + const regionOptions: IDropdownOption[] = []; + regionOptions.push({ + key: userContext?.databaseAccount?.properties?.documentEndpoint, + text: `Global (Default)`, + data: { + isGlobal: true, + writeEnabled: true, + }, + }); + userContext?.databaseAccount?.properties?.writeLocations?.forEach((loc) => { + if (!uniqueAccountRegions.has(loc.locationName)) { + uniqueAccountRegions.add(loc.locationName); + regionOptions.push({ + key: loc.documentEndpoint, + text: `${loc.locationName} (Read/Write)`, + data: { + isGlobal: false, + writeEnabled: true, + }, + }); + } + }); + userContext?.databaseAccount?.properties?.readLocations?.forEach((loc) => { + if (!uniqueAccountRegions.has(loc.locationName)) { + uniqueAccountRegions.add(loc.locationName); + regionOptions.push({ + key: loc.documentEndpoint, + text: `${loc.locationName} (Read)`, + data: { + isGlobal: false, + writeEnabled: false, + }, + }); + } + }); + const shouldShowCopilotSampleDBOption = userContext.apiType === "SQL" && useQueryCopilot.getState().copilotEnabled && @@ -274,6 +334,46 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ } } + const storedRegionalEndpoint = loadState({ + componentName: AppStateComponentNames.SelectedRegionalEndpoint, + globalAccountName: userContext.databaseAccount?.name, + }) as string; + const selectedRegionIsGlobal = + selectedRegionalEndpoint === userContext?.databaseAccount?.properties?.documentEndpoint; + if (selectedRegionIsGlobal && storedRegionalEndpoint) { + deleteState({ + componentName: AppStateComponentNames.SelectedRegionalEndpoint, + globalAccountName: userContext.databaseAccount?.name, + }); + updateUserContext({ + selectedRegionalEndpoint: undefined, + writeEnabledInSelectedRegion: true, + refreshCosmosClient: true, + }); + useClientWriteEnabled.setState({ clientWriteEnabled: true }); + } else if ( + selectedRegionalEndpoint && + !selectedRegionIsGlobal && + selectedRegionalEndpoint !== storedRegionalEndpoint + ) { + saveState( + { + componentName: AppStateComponentNames.SelectedRegionalEndpoint, + globalAccountName: userContext.databaseAccount?.name, + }, + selectedRegionalEndpoint, + ); + const validWriteEndpoint = userContext.databaseAccount?.properties?.writeLocations?.find( + (loc) => loc.documentEndpoint === selectedRegionalEndpoint, + ); + updateUserContext({ + selectedRegionalEndpoint: selectedRegionalEndpoint, + writeEnabledInSelectedRegion: !!validWriteEndpoint, + refreshCosmosClient: true, + }); + useClientWriteEnabled.setState({ clientWriteEnabled: !!validWriteEndpoint }); + } + LocalStorageUtility.setEntryBoolean(StorageKey.RUThresholdEnabled, ruThresholdEnabled); LocalStorageUtility.setEntryBoolean(StorageKey.QueryTimeoutEnabled, queryTimeoutEnabled); LocalStorageUtility.setEntryNumber(StorageKey.RetryAttempts, retryAttempts); @@ -423,6 +523,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ setDefaultQueryResultsView(option.key as SplitterDirection); }; + const handleOnSelectedRegionOptionChange = (ev: React.FormEvent, option: IDropdownOption): void => { + setSelectedRegionalEndpoint(option.key as string); + }; + const handleOnQueryRetryAttemptsSpinButtonChange = (ev: React.MouseEvent, newValue?: string): void => { const retryAttempts = Number(newValue); if (!isNaN(retryAttempts)) { @@ -583,9 +687,39 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ )} + {userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && ( + + +
Region Selection
+
+ +
+
+ Changes region the Cosmos Client uses to access account. +
+
+ Select Region + + Changes the account endpoint used to perform client operations. + +
+ option.key === selectedRegionalEndpoint)?.text + : regionOptions[0]?.text + } + onChange={handleOnSelectedRegionOptionChange} + options={regionOptions} + styles={{ root: { marginBottom: "10px" } }} + /> +
+
+
+ )} {userContext.apiType === "SQL" && !isEmulator && ( <> - +
Query Timeout
@@ -626,7 +760,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
- +
RU Limit
@@ -660,7 +794,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
- +
Default Query Results View
@@ -681,8 +815,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)} + {showRetrySettings && ( - +
Retry Settings
@@ -755,7 +890,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)} {!isEmulator && ( - +
Enable container pagination
@@ -779,7 +914,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)} {shouldShowCrossPartitionOption && ( - +
Enable cross-partition query
@@ -804,7 +939,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)} {shouldShowParallelismOption && ( - +
Max degree of parallelism
@@ -837,7 +972,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)} {shouldShowPriorityLevelOption && ( - +
Priority Level
@@ -860,7 +995,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)} {shouldShowGraphAutoVizOption && ( - +
Display Gremlin query results as: 
@@ -881,7 +1016,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)} {shouldShowCopilotSampleDBOption && ( - +
Enable sample database
@@ -916,7 +1051,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ "Clear History", undefined, "Are you sure you want to proceed?", - () => deleteAllStates(), + () => { + deleteAllStates(); + updateUserContext({ + selectedRegionalEndpoint: undefined, + writeEnabledInSelectedRegion: true, + refreshCosmosClient: true, + }); + useClientWriteEnabled.setState({ clientWriteEnabled: true }); + }, "Cancel", undefined, <> @@ -927,6 +1070,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
  • Reset your customized tab layout, including the splitter positions
  • Erase your table column preferences, including any custom columns
  • Clear your filter history
  • +
  • Reset region selection to global
  • , ); diff --git a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap index e89ee345b..577b6de5b 100644 --- a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap +++ b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap @@ -107,7 +107,7 @@ exports[`Settings Pane should render Default properly 1`] = `
    ; editorState: ViewModels.DocumentExplorerState; isPreferredApiMongoDB: boolean; + clientWriteEnabled: boolean; onNewDocumentClick: UiKeyboardEvent; onSaveNewDocumentClick: UiKeyboardEvent; onRevertNewDocumentClick: UiKeyboardEvent; @@ -328,6 +330,7 @@ const createUploadButton = (container: Explorer): CommandButtonComponentProps => hasPopup: true, disabled: useSelectedNode.getState().isDatabaseNodeOrNoneSelected() || + !useClientWriteEnabled.getState().clientWriteEnabled || useSelectedNode.getState().isQueryCopilotCollectionSelected(), }; }; @@ -346,6 +349,7 @@ export const getTabsButtons = ({ selectedRows, editorState, isPreferredApiMongoDB, + clientWriteEnabled, onNewDocumentClick, onSaveNewDocumentClick, onRevertNewDocumentClick, @@ -371,6 +375,7 @@ export const getTabsButtons = ({ hasPopup: false, disabled: !getNewDocumentButtonState(editorState).enabled || + !clientWriteEnabled || useSelectedNode.getState().isQueryCopilotCollectionSelected(), id: NEW_DOCUMENT_BUTTON_ID, }); @@ -388,6 +393,7 @@ export const getTabsButtons = ({ hasPopup: false, disabled: !getSaveNewDocumentButtonState(editorState).enabled || + !clientWriteEnabled || useSelectedNode.getState().isQueryCopilotCollectionSelected(), id: SAVE_BUTTON_ID, }); @@ -422,6 +428,7 @@ export const getTabsButtons = ({ hasPopup: false, disabled: !getSaveExistingDocumentButtonState(editorState).enabled || + !clientWriteEnabled || useSelectedNode.getState().isQueryCopilotCollectionSelected(), id: UPDATE_BUTTON_ID, }); @@ -454,7 +461,7 @@ export const getTabsButtons = ({ commandButtonLabel: label, ariaLabel: label, hasPopup: false, - disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected(), + disabled: useSelectedNode.getState().isQueryCopilotCollectionSelected() || !clientWriteEnabled, id: DELETE_BUTTON_ID, }); } @@ -628,6 +635,7 @@ export const DocumentsTabComponent: React.FunctionComponent state.clientWriteEnabled); const [tabStateData, setTabStateData] = useState(() => readDocumentsTabSubComponentState(SubComponentName.MainTabDivider, _collection, { leftPaneWidthPercent: 35, @@ -865,6 +873,7 @@ export const DocumentsTabComponent: React.FunctionComponent void; + private unsubscribeClientWriteEnabled: () => void; componentDidMount(): void { useTabs.subscribe((state: TabsState) => { @@ -712,10 +717,17 @@ class QueryTabComponentImpl extends React.Component { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + }); } componentWillUnmount(): void { document.removeEventListener("keydown", this.handleCopilotKeyDown); + if (this.unsubscribeClientWriteEnabled) { + this.unsubscribeClientWriteEnabled(); + } } private getEditorAndQueryResult(): JSX.Element { diff --git a/src/Shared/AppStatePersistenceUtility.ts b/src/Shared/AppStatePersistenceUtility.ts index 58a02c3b3..ed354ef06 100644 --- a/src/Shared/AppStatePersistenceUtility.ts +++ b/src/Shared/AppStatePersistenceUtility.ts @@ -10,6 +10,7 @@ export enum AppStateComponentNames { MostRecentActivity = "MostRecentActivity", QueryCopilot = "QueryCopilot", DataExplorerAction = "DataExplorerAction", + SelectedRegionalEndpoint = "SelectedRegionalEndpoint", } // Subcomponent for DataExplorerAction diff --git a/src/UserContext.ts b/src/UserContext.ts index 6569d5e18..8a880f498 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -111,6 +111,8 @@ export interface UserContext { readonly isReplica?: boolean; collectionCreationDefaults: CollectionCreationDefaults; sampleDataConnectionInfo?: ParsedResourceTokenConnectionString; + readonly selectedRegionalEndpoint?: string; + readonly writeEnabledInSelectedRegion?: boolean; readonly vcoreMongoConnectionParams?: VCoreMongoConnectionParams; readonly feedbackPolicies?: AdminFeedbackPolicySettings; readonly dataPlaneRbacEnabled?: boolean; diff --git a/src/hooks/useClientWriteEnabled.ts b/src/hooks/useClientWriteEnabled.ts new file mode 100644 index 000000000..7d9d29c2e --- /dev/null +++ b/src/hooks/useClientWriteEnabled.ts @@ -0,0 +1,10 @@ +import create, { UseStore } from "zustand"; +interface ClientWriteEnabledState { + clientWriteEnabled: boolean; + setClientWriteEnabled: (writeEnabled: boolean) => void; +} + +export const useClientWriteEnabled: UseStore = create((set) => ({ + clientWriteEnabled: true, + setClientWriteEnabled: (clientWriteEnabled: boolean) => set({ clientWriteEnabled }), +})); diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index 2e29d1363..71818f572 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -17,12 +17,16 @@ import { useSelectedNode } from "Explorer/useSelectedNode"; import { isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil"; import { AppStateComponentNames, + deleteState, + hasState, + loadState, OPEN_TABS_SUBCOMPONENT_NAME, readSubComponentState, } from "Shared/AppStatePersistenceUtility"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { isDataplaneRbacSupported } from "Utils/APITypeUtils"; import { logConsoleError } from "Utils/NotificationConsoleUtils"; +import { useClientWriteEnabled } from "hooks/useClientWriteEnabled"; import { useQueryCopilot } from "hooks/useQueryCopilot"; import { ReactTabKind, useTabs } from "hooks/useTabs"; import { useEffect, useState } from "react"; @@ -345,6 +349,9 @@ async function configureHostedWithAAD(config: AAD): Promise { `Configuring Data Explorer for ${userContext.apiType} account ${account.name}`, "Explorer/configureHostedWithAAD", ); + if (userContext.apiType === "SQL") { + checkAndUpdateSelectedRegionalEndpoint(); + } if (!userContext.features.enableAadDataPlane) { Logger.logInfo(`AAD Feature flag is not enabled for account ${account.name}`, "Explorer/configureHostedWithAAD"); if (isDataplaneRbacSupported(userContext.apiType)) { @@ -706,6 +713,10 @@ async function configurePortal(): Promise { const { databaseAccount: account, subscriptionId, resourceGroup } = userContext; + if (userContext.apiType === "SQL") { + checkAndUpdateSelectedRegionalEndpoint(); + } + let dataPlaneRbacEnabled; if (isDataplaneRbacSupported(userContext.apiType)) { if (LocalStorageUtility.hasItem(StorageKey.DataPlaneRbacEnabled)) { @@ -824,6 +835,41 @@ function updateAADEndpoints(portalEnv: PortalEnv) { } } +function checkAndUpdateSelectedRegionalEndpoint() { + const accountName = userContext.databaseAccount?.name; + if (hasState({ componentName: AppStateComponentNames.SelectedRegionalEndpoint, globalAccountName: accountName })) { + const storedRegionalEndpoint = loadState({ + componentName: AppStateComponentNames.SelectedRegionalEndpoint, + globalAccountName: accountName, + }) as string; + const validEndpoint = userContext.databaseAccount?.properties?.readLocations?.find( + (loc) => loc.documentEndpoint === storedRegionalEndpoint, + ); + const validWriteEndpoint = userContext.databaseAccount?.properties?.writeLocations?.find( + (loc) => loc.documentEndpoint === storedRegionalEndpoint, + ); + if (validEndpoint) { + updateUserContext({ + selectedRegionalEndpoint: storedRegionalEndpoint, + writeEnabledInSelectedRegion: !!validWriteEndpoint, + refreshCosmosClient: true, + }); + useClientWriteEnabled.setState({ clientWriteEnabled: !!validWriteEndpoint }); + } else { + deleteState({ componentName: AppStateComponentNames.SelectedRegionalEndpoint, globalAccountName: accountName }); + updateUserContext({ + writeEnabledInSelectedRegion: true, + }); + useClientWriteEnabled.setState({ clientWriteEnabled: true }); + } + } else { + updateUserContext({ + writeEnabledInSelectedRegion: true, + }); + useClientWriteEnabled.setState({ clientWriteEnabled: true }); + } +} + function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) { if ( configContext.PORTAL_BACKEND_ENDPOINT && From 0666e11d893b54e68cafd5396f3ad380bb784c90 Mon Sep 17 00:00:00 2001 From: Ajay Parulekar <79739098+ajparule@users.noreply.github.com> Date: Wed, 9 Apr 2025 23:02:35 +0530 Subject: [PATCH 05/87] Renaming Materialized views builder blade text to Global secondary indexes for NoSql API (#1991) * GSI changes * GSI changes * GSI changes * updating GlobalSecondaryIndexesBuilder.json * Changes * Update cost text keys based on user context API type * Refactor Materialized Views Builder code for improved readability and consistency in API type checks * Update links in Materialized Views Builder for consistency and accuracy * Update Global Secondary Indexes links and descriptions for clarity and accuracy based on API type * Update portal notification message keys based on user context API type for Materialized Views Builder * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Fix capitalization and wording inconsistencies in Materialized Views Builder localization strings * Fix capitalization and wording inconsistencies in localization strings for Materialized Views Builder * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi * Update src/Localization/en/MaterializedViewsBuilder.json Co-authored-by: Justine Cocchi --------- Co-authored-by: Justine Cocchi --- .../en/MaterializedViewsBuilder.json | 69 ++++++++++----- .../MaterializedViewsBuilder.rp.ts | 20 ++++- .../MaterializedViewsBuilder.tsx | 84 +++++++++++++------ 3 files changed, 121 insertions(+), 52 deletions(-) diff --git a/src/Localization/en/MaterializedViewsBuilder.json b/src/Localization/en/MaterializedViewsBuilder.json index 7c3aaa032..0400122d9 100644 --- a/src/Localization/en/MaterializedViewsBuilder.json +++ b/src/Localization/en/MaterializedViewsBuilder.json @@ -1,11 +1,11 @@ { - "MaterializedViewsBuilderDescription": "Provision a Materializedviews builder cluster for your Azure Cosmos DB account. Materializedviews builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition.", - "MaterializedViewsBuilder": "Materializedviews Builder", + "MaterializedViewsBuilderDescription": "Provision a materialized views builder for your Azure Cosmos DB account. Materialized views builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materialized view definition.", + "MaterializedViewsBuilder": "Materialized views builder", "Provisioned": "Provisioned", "Deprovisioned": "Deprovisioned", - "LearnAboutMaterializedViews": "Learn more about materializedviews.", - "DeprovisioningDetailsText": "Learn more about materializedviews.", - "MaterializedviewsBuilderPricing": "Learn more about materializedviews pricing.", + "LearnAboutMaterializedViews": "Learn more about materialized views.", + "DeprovisioningDetailsText": "Learn more about materialized views.", + "MaterializedviewsBuilderPricing": "Learn more about materialized views pricing.", "SKUs": "SKUs", "SKUsPlaceHolder": "Select SKUs", "NumberOfInstances": "Number of instances", @@ -14,35 +14,58 @@ "CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)", "CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)", "CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)", - "CreateMessage": "MaterializedViewsBuilder resource is being created.", + "CreateMessage": "Materialized views builder resource is being created.", "CreateInitializeTitle": "Provisioning resource", - "CreateInitializeMessage": "Materializedviews Builder resource will be provisioned.", + "CreateInitializeMessage": "Materialized views builder resource will be provisioned.", "CreateSuccessTitle": "Resource provisioned", - "CreateSuccesseMessage": "Materializedviews Builder resource provisioned.", + "CreateSuccesseMessage": "Materialized views builder resource provisioned.", "CreateFailureTitle": "Failed to provision resource", - "CreateFailureMessage": "Materializedviews Builder resource provisioning failed.", - "UpdateMessage": "MaterializedViewsBuilder resource is being updated.", + "CreateFailureMessage": "Materialized views builder resource provisioning failed.", + "UpdateMessage": "Materialized views builder resource is being updated.", "UpdateInitializeTitle": "Updating resource", - "UpdateInitializeMessage": "Materializedviews Builder resource will be updated.", + "UpdateInitializeMessage": "Materialized views builder resource will be updated.", "UpdateSuccessTitle": "Resource updated", - "UpdateSuccesseMessage": "Materializedviews Builder resource updated.", + "UpdateSuccesseMessage": "Materialized views builder resource updated.", "UpdateFailureTitle": "Failed to update resource", - "UpdateFailureMessage": "Materializedviews Builder resource updation failed.", - "DeleteMessage": "MaterializedViewsBuilder resource is being deleted.", + "UpdateFailureMessage": "Materialized views builder resource update failed.", + "DeleteMessage": "Materialized views builder resource is being deleted.", "DeleteInitializeTitle": "Deleting resource", - "DeleteInitializeMessage": "Materializedviews Builder resource will be deleted.", + "DeleteInitializeMessage": "Materialized views builder resource will be deleted.", "DeleteSuccessTitle": "Resource deleted", - "DeleteSuccesseMessage": "Materializedviews Builder resource deleted.", + "DeleteSuccesseMessage": "Materialized views builder resource deleted.", "DeleteFailureTitle": "Failed to delete resource", - "DeleteFailureMessage": "Materializedviews Builder resource deletion failed.", + "DeleteFailureMessage": "Materialized views builder resource deletion failed.", "ApproximateCost": "Approximate Cost Per Hour", - "CostText": "Hourly cost of the Materializedviews Builder resource depends on the SKU selection, number of instances per region, and number of regions.", + "CostText": "Hourly cost of the materialized views builder resource depends on the SKU selection, number of instances per region, and number of regions.", "MetricsString": "Metrics", - "MetricsText": "Monitor the CPU and memory usage for the Materializedviews Builder instances in ", + "MetricsText": "Monitor the CPU and memory usage for the materialized views builder instances in ", "MetricsBlade": "the metrics blade.", "MonitorUsage": "Monitor Usage", - "ResizingDecisionText": "To understand if the Materializedviews Builder is the right size, ", - "ResizingDecisionLink": "learn more about Materializedviews Builder sizing.", - "WarningBannerOnUpdate": "Adding or modifying Materializedviews Builder instances may affect your bill.", - "WarningBannerOnDelete": "After deprovisioning the Materializedviews Builder, your materializedviews will not be updated with new source changes anymore. Materializedviews builder is compute in your account that performs read operations on source collection for any updates and applies them on materialized views as per the materializedview definition." + "ResizingDecisionText": "To understand if the materialized views builder is the right size, ", + "ResizingDecisionLink": "learn more about materialized views builder sizing.", + "WarningBannerOnUpdate": "Adding or modifying materialized views builder instances may affect your bill.", + "WarningBannerOnDelete": "After deprovisioning the materialized views builder, your materialized views will not be updated with new source changes anymore. Materialized views builder is compute in your account that performs read operations on source containers for any updates and applies them on materialized views as per the materialized view definition.", + "GlobalsecondaryindexesBuilderDescription": "Provision a global secondary indexes builder for your Azure Cosmos DB account. The global secondary indexes builder is compute in your account that performs read operations on source collections for any updates and populates the global secondary indexes as per their definition.", + "GlobalsecondaryindexesBuilder": "Global secondary indexes builder", + "LearnAboutGlobalSecondaryIndexes": "Learn more about global secondary indexes.", + "GlobalsecondaryindexesDeprovisioningDetailsText": "Learn more about global secondary indexes.", + "GlobalsecondaryindexesBuilderPricing": "Learn more about global secondary indexes pricing.", + "GlobalsecondaryindexesCreateMessage": "Global secondary indexes builder resource is being created.", + "GlobalsecondaryindexesCreateInitializeMessage": "Global secondary indexes builder resource will be provisioned.", + "GlobalsecondaryindexesCreateSuccesseMessage": "Global secondary indexes builder resource provisioned.", + "GlobalsecondaryindexesCreateFailureMessage": "Global secondary indexes builder resource provisioning failed.", + "GlobalsecondaryindexesUpdateMessage": "Global secondary indexes builder resource is being updated.", + "GlobalsecondaryindexesUpdateInitializeMessage": "Global secondary indexes builder resource will be updated.", + "GlobalsecondaryindexesUpdateSuccesseMessage": "Global secondary indexes builder resource updated.", + "GlobalsecondaryindexesUpdateFailureMessage": "Global secondary indexes builder resource update failed.", + "GlobalsecondaryindexesDeleteMessage": "Global secondary indexes builder resource is being deleted.", + "GlobalsecondaryindexesDeleteInitializeMessage": "Global secondary indexes builder resource will be deleted.", + "GlobalsecondaryindexesDeleteSuccesseMessage": "Global secondary indexes builder resource deleted.", + "GlobalsecondaryindexesDeleteFailureMessage": "Global secondary indexes builder resource deletion failed.", + "GlobalsecondaryindexesCostText": "Hourly cost of the global secondary indexes builder resource depends on the SKU selection, number of instances per region, and number of regions.", + "GlobalsecondaryindexesMetricsText": "Monitor the CPU and memory usage for the global secondary indexes builder instances in ", + "GlobalsecondaryindexesResizingDecisionText": "To understand if the global secondary indexes builder is the right size, ", + "GlobalsecondaryindexesesizingDecisionLink": "learn more about global secondary indexes builder sizing.", + "GlobalsecondaryindexesWarningBannerOnUpdate": "Adding or modifying global secondary indexes builder instances may affect your bill.", + "GlobalsecondaryindexesWarningBannerOnDelete": "After deprovisioning the global secondary indexes builder, your global secondary indexes will no longer be updated with new source changes. Global secondary indexes builder is compute in your account that performs read operations on source containers for any updates and applies them on global secondary indexes as per their definition." } \ No newline at end of file diff --git a/src/SelfServe/MaterializedViewsBuilder/MaterializedViewsBuilder.rp.ts b/src/SelfServe/MaterializedViewsBuilder/MaterializedViewsBuilder.rp.ts index d5fae8d0d..59833e912 100644 --- a/src/SelfServe/MaterializedViewsBuilder/MaterializedViewsBuilder.rp.ts +++ b/src/SelfServe/MaterializedViewsBuilder/MaterializedViewsBuilder.rp.ts @@ -6,9 +6,9 @@ import { RefreshResult } from "../SelfServeTypes"; import MaterializedViewsBuilder from "./MaterializedViewsBuilder"; import { FetchPricesResponse, + MaterializedViewsBuilderServiceResource, PriceMapAndCurrencyCode, RegionsResponse, - MaterializedViewsBuilderServiceResource, UpdateMaterializedViewsBuilderRequestParameters, } from "./MaterializedViewsBuilderTypes"; @@ -123,11 +123,23 @@ export const refreshMaterializedViewsBuilderProvisioning = async (): Promise => { }; const NumberOfInstancesDropdownInfo: Info = { - messageTKey: "ResizingDecisionText", + messageTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesResizingDecisionText" : "ResizingDecisionText", link: { href: "https://aka.ms/cosmos-db-materializedviewsbuilder-size", - textTKey: "ResizingDecisionLink", + textTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesesizingDecisionLink" : "ResizingDecisionLink", }, }; const ApproximateCostDropDownInfo: Info = { - messageTKey: "CostText", + messageTKey: userContext.apiType === "SQL" ? "GlobalsecondaryindexesCostText" : "CostText", link: { href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing", - textTKey: "MaterializedviewsBuilderPricing", + textTKey: + userContext.apiType === "SQL" ? "GlobalsecondaryindexesBuilderPricing" : "MaterializedviewsBuilderPricing", }, }; @@ -268,15 +281,20 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass { portalNotification: { initialize: { titleTKey: "DeleteInitializeTitle", - messageTKey: "DeleteInitializeMessage", + messageTKey: + userContext.apiType === "SQL" + ? "GlobalsecondaryindexesDeleteInitializeMessage" + : "DeleteInitializeMessage", }, success: { titleTKey: "DeleteSuccessTitle", - messageTKey: "DeleteSuccesseMessage", + messageTKey: + userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteSuccesseMessage" : "DeleteSuccesseMessage", }, failure: { titleTKey: "DeleteFailureTitle", - messageTKey: "DeleteFailureMessage", + messageTKey: + userContext.apiType === "SQL" ? "GlobalsecondaryindexesDeleteFailureMessage" : "DeleteFailureMessage", }, }, }; @@ -289,15 +307,20 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass { portalNotification: { initialize: { titleTKey: "UpdateInitializeTitle", - messageTKey: "UpdateInitializeMessage", + messageTKey: + userContext.apiType === "SQL" + ? "GlobalsecondaryindexesUpdateInitializeMessage" + : "UpdateInitializeMessage", }, success: { titleTKey: "UpdateSuccessTitle", - messageTKey: "UpdateSuccesseMessage", + messageTKey: + userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateSuccesseMessage" : "UpdateSuccesseMessage", }, failure: { titleTKey: "UpdateFailureTitle", - messageTKey: "UpdateFailureMessage", + messageTKey: + userContext.apiType === "SQL" ? "GlobalsecondaryindexesUpdateFailureMessage" : "UpdateFailureMessage", }, }, }; @@ -311,15 +334,20 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass { portalNotification: { initialize: { titleTKey: "CreateInitializeTitle", - messageTKey: "CreateInitializeMessage", + messageTKey: + userContext.apiType === "SQL" + ? "GlobalsecondaryindexesCreateInitializeMessage" + : "CreateInitializeMessage", }, success: { titleTKey: "CreateSuccessTitle", - messageTKey: "CreateSuccesseMessage", + messageTKey: + userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateSuccesseMessage" : "CreateSuccesseMessage", }, failure: { titleTKey: "CreateFailureTitle", - messageTKey: "CreateFailureMessage", + messageTKey: + userContext.apiType === "SQL" ? "GlobalsecondaryindexesCreateFailureMessage" : "CreateFailureMessage", }, }, }; @@ -366,11 +394,17 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass { @Values({ description: { - textTKey: "MaterializedViewsBuilderDescription", + textTKey: + userContext.apiType === "SQL" + ? "GlobalsecondaryindexesBuilderDescription" + : "MaterializedViewsBuilderDescription", type: DescriptionType.Text, link: { - href: "https://aka.ms/cosmos-db-materializedviews", - textTKey: "LearnAboutMaterializedViews", + href: + userContext.apiType === "SQL" + ? "https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views" + : "https://learn.microsoft.com/en-us/azure/cosmos-db/cassandra/materialized-views", + textTKey: userContext.apiType === "SQL" ? "LearnAboutGlobalSecondaryIndexes" : "LearnAboutMaterializedViews", }, }, }) @@ -378,7 +412,7 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass { @OnChange(onEnableMaterializedViewsBuilderChange) @Values({ - labelTKey: "MaterializedViewsBuilder", + labelTKey: userContext.apiType === "SQL" ? "GlobalSecondaryIndexesBuilder" : "MaterializedViewsBuilder", trueLabelTKey: "Provisioned", falseLabelTKey: "Deprovisioned", }) From a3bfc89318f6c819cec4eb3d2b38aa8a179e285c Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Wed, 9 Apr 2025 13:04:44 -0500 Subject: [PATCH 06/87] Revert "Pk missing fix (#2094)" (#2099) This reverts commit af0a890516b6302b60710b4f54e58b68d88b1857. --- playwright.config.ts | 1 + .../Tabs/DocumentsTabV2/DocumentsTabV2.tsx | 17 +--- .../DocumentsTabV2.test.tsx.snap | 4 - src/Utils/QueryUtils.test.ts | 78 +++------------- src/Utils/QueryUtils.ts | 14 +-- test/fx.ts | 33 +------ test/sql/document.spec.ts | 45 ---------- test/sql/testCases.ts | 88 ------------------- 8 files changed, 21 insertions(+), 259 deletions(-) delete mode 100644 test/sql/document.spec.ts delete mode 100644 test/sql/testCases.ts diff --git a/playwright.config.ts b/playwright.config.ts index 5279a0660..4c5ad3c14 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,4 +1,5 @@ import { defineConfig, devices } from "@playwright/test"; + /** * See https://playwright.dev/docs/test-configuration. */ diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index 65424d881..cef323b0e 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -1046,7 +1046,6 @@ export const DocumentsTabComponent: React.FunctionComponent { - // in case of any kind of failures of accidently changing partition key, restore the original - // so that when user navigates away from current document and comes back, - // it doesnt fail to load due to using the invalid partition keys - selectedDocumentId.partitionKeyValue = originalPartitionKeyValue; onExecutionErrorChange(true); const errorMessage = getErrorMessage(error); useDialog.getState().showOkModalDialog("Update document failed", errorMessage); @@ -2099,8 +2094,8 @@ export const DocumentsTabComponent: React.FunctionComponent -
    -
    +
    +
    {!isPreferredApiMongoDB && SELECT * FROM c } -
    +
    -
    +
    {isTabActive && selectedDocumentContent && selectedRows.size <= 1 && (
    SELECT * FROM c @@ -67,7 +65,6 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = ` preferredSize="35%" >
    { version: 2, }; }; - const generatePartitionKeysForPaths = (paths: string[]): DataModels.PartitionKey => { - return { - paths: paths, - kind: "Hash", - version: 2, - }; - }; describe("buildDocumentsQueryPartitionProjections()", () => { it("should return empty string if partition key is undefined", () => { @@ -96,18 +89,6 @@ describe("Query Utils", () => { expect(query).toContain("c.id"); }); - - it("should always include {} for any missing partition keys", () => { - const query = QueryUtils.buildDocumentsQuery( - "", - ["a", "b", "c"], - generatePartitionKeysForPaths(["/a", "/b", "/c"]), - [], - ); - expect(query).toContain('IIF(IS_DEFINED(c["a"]), c["a"], {})'); - expect(query).toContain('IIF(IS_DEFINED(c["b"]), c["b"], {})'); - expect(query).toContain('IIF(IS_DEFINED(c["c"]), c["c"], {})'); - }); }); describe("queryPagesUntilContentPresent()", () => { @@ -220,6 +201,18 @@ describe("Query Utils", () => { expect(expectedPartitionKeyValues).toContain(documentContent["Category"]); }); + it("should extract no partition key values in the case nested partition key", () => { + const singlePartitionKeyDefinition: PartitionKeyDefinition = { + kind: PartitionKeyKind.Hash, + paths: ["/Location.type"], + }; + const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues( + documentContent, + singlePartitionKeyDefinition, + ); + expect(partitionKeyValues.length).toBe(0); + }); + it("should extract all partition key values for hierarchical and nested partition keys", () => { const mixedPartitionKeyDefinition: PartitionKeyDefinition = { kind: PartitionKeyKind.MultiHash, @@ -232,52 +225,5 @@ describe("Query Utils", () => { expect(partitionKeyValues.length).toBe(2); expect(partitionKeyValues).toEqual(["United States", "Point"]); }); - - it("if any partition key is null or empty string, the partitionKeyValues shall match", () => { - const newDocumentContent = { - ...documentContent, - ...{ - Country: null, - Location: { - type: "", - coordinates: [-121.49, 46.206], - }, - }, - }; - - const mixedPartitionKeyDefinition: PartitionKeyDefinition = { - kind: PartitionKeyKind.MultiHash, - paths: ["/Country", "/Location/type"], - }; - const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues( - newDocumentContent, - mixedPartitionKeyDefinition, - ); - expect(partitionKeyValues.length).toBe(2); - expect(partitionKeyValues).toEqual([null, ""]); - }); - - it("if any partition key doesn't exist, it should still set partitionkey value as {}", () => { - const newDocumentContent = { - ...documentContent, - ...{ - Country: null, - Location: { - coordinates: [-121.49, 46.206], - }, - }, - }; - - const mixedPartitionKeyDefinition: PartitionKeyDefinition = { - kind: PartitionKeyKind.MultiHash, - paths: ["/Country", "/Location/type"], - }; - const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues( - newDocumentContent, - mixedPartitionKeyDefinition, - ); - expect(partitionKeyValues.length).toBe(2); - expect(partitionKeyValues).toEqual([null, {}]); - }); }); }); diff --git a/src/Utils/QueryUtils.ts b/src/Utils/QueryUtils.ts index 07822a422..f0b39e4e2 100644 --- a/src/Utils/QueryUtils.ts +++ b/src/Utils/QueryUtils.ts @@ -47,7 +47,6 @@ export function buildDocumentsQueryPartitionProjections( for (const index in partitionKey.paths) { // TODO: Handle "/" in partition key definitions const projectedProperties: string[] = partitionKey.paths[index].split("/").slice(1); - const isSystemPartitionKey: boolean = partitionKey.systemKey || false; let projectedProperty = ""; projectedProperties.forEach((property: string) => { @@ -62,13 +61,8 @@ export function buildDocumentsQueryPartitionProjections( projectedProperty += `[${projection}]`; } }); - const fullAccess = `${collectionAlias}${projectedProperty}`; - if (!isSystemPartitionKey) { - const wrappedProjection = `IIF(IS_DEFINED(${fullAccess}), ${fullAccess}, {})`; - projections.push(wrappedProjection); - } else { - projections.push(fullAccess); - } + + projections.push(`${collectionAlias}${projectedProperty}`); } return projections.join(","); @@ -124,7 +118,7 @@ export const extractPartitionKeyValues = ( documentContent: any, partitionKeyDefinition: PartitionKeyDefinition, ): PartitionKey[] => { - if (!partitionKeyDefinition.paths || partitionKeyDefinition.paths.length === 0 || partitionKeyDefinition.systemKey) { + if (!partitionKeyDefinition.paths || partitionKeyDefinition.paths.length === 0) { return undefined; } @@ -136,8 +130,6 @@ export const extractPartitionKeyValues = ( if (value !== undefined) { partitionKeyValues.push(value); - } else { - partitionKeyValues.push({}); } }); diff --git a/test/fx.ts b/test/fx.ts index 14ad0acbb..661d55897 100644 --- a/test/fx.ts +++ b/test/fx.ts @@ -26,7 +26,7 @@ export function getAzureCLICredentials(): AzureCliCredential { export async function getAzureCLICredentialsToken(): Promise { const credentials = getAzureCLICredentials(); - const token = (await credentials.getToken("https://management.core.windows.net//.default"))?.token || ""; + const token = (await credentials.getToken("https://management.core.windows.net//.default")).token; return token; } @@ -37,7 +37,6 @@ export enum TestAccount { Mongo = "Mongo", Mongo32 = "Mongo32", SQL = "SQL", - SQLReadOnly = "SQLReadOnly", } export const defaultAccounts: Record = { @@ -47,7 +46,6 @@ export const defaultAccounts: Record = { [TestAccount.Mongo]: "github-e2etests-mongo", [TestAccount.Mongo32]: "github-e2etests-mongo32", [TestAccount.SQL]: "github-e2etests-sql", - [TestAccount.SQLReadOnly]: "github-e2etests-sql-readonly", }; export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests"; @@ -216,25 +214,6 @@ export class QueryTab { } } -export class DocumentsTab { - documentsFilter: Locator; - documentsListPane: Locator; - documentResultsPane: Locator; - resultsEditor: Editor; - - constructor( - public frame: Frame, - public tabId: string, - public tab: Locator, - public locator: Locator, - ) { - this.documentsFilter = this.locator.getByTestId("DocumentsTab/Filter"); - this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane"); - this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane"); - this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded")); - } -} - type PanelOpenOptions = { closeTimeout?: number; }; @@ -253,12 +232,6 @@ export class DataExplorer { return new QueryTab(this.frame, tabId, tab, queryTab); } - documentsTab(tabId: string): DocumentsTab { - const tab = this.tab(tabId); - const documentsTab = tab.getByTestId("DocumentsTab"); - return new DocumentsTab(this.frame, tabId, tab, documentsTab); - } - /** Select the primary global command button. * * There's only a single "primary" button, but we still require you to pass the label to confirm you're selecting the right button. @@ -321,10 +294,6 @@ export class DataExplorer { return await this.waitForNode(`${databaseId}/${containerId}`); } - async waitForContainerItemsNode(databaseId: string, containerId: string): Promise { - return await this.waitForNode(`${databaseId}/${containerId}/Items`); - } - /** Select the tree node with the specified id */ treeNode(id: string): TreeNode { return new TreeNode(this.frame.getByTestId(`TreeNode:${id}`), this.frame, id); diff --git a/test/sql/document.spec.ts b/test/sql/document.spec.ts deleted file mode 100644 index 10877702a..000000000 --- a/test/sql/document.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { expect, test } from "@playwright/test"; - -import { DataExplorer, DocumentsTab, TestAccount } from "../fx"; -import { documentTestCases } from "./testCases"; - -let explorer: DataExplorer = null!; -let documentsTab: DocumentsTab = null!; - -for (const { name, databaseId, containerId, expectedDocumentIds } of documentTestCases) { - test.describe(`Test Documents with ${name}`, () => { - test.beforeEach("Open documents tab", async ({ page }) => { - explorer = await DataExplorer.open(page, TestAccount.SQLReadOnly); - - const containerNode = await explorer.waitForContainerNode(databaseId, containerId); - await containerNode.expand(); - - const containerMenuNode = await explorer.waitForContainerItemsNode(databaseId, containerId); - await containerMenuNode.element.click(); - - documentsTab = explorer.documentsTab("tab0"); - - await documentsTab.documentsFilter.waitFor(); - await documentsTab.documentsListPane.waitFor(); - await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 }); - }); - - for (const docId of expectedDocumentIds) { - test.describe(`Document ID: ${docId}`, () => { - test(`should load and view document ${docId}`, async () => { - const span = documentsTab.documentsListPane.getByText(docId, { exact: true }).nth(0); - await span.waitFor(); - await expect(span).toBeVisible(); - - await span.click(); - await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 }); - - const resultText = await documentsTab.resultsEditor.text(); - const resultData = JSON.parse(resultText!); - expect(resultText).not.toBeNull(); - expect(resultData?.id).toEqual(docId); - }); - }); - } - }); -} diff --git a/test/sql/testCases.ts b/test/sql/testCases.ts deleted file mode 100644 index 0f18bb183..000000000 --- a/test/sql/testCases.ts +++ /dev/null @@ -1,88 +0,0 @@ -type ContainerTestCase = { - name: string; - databaseId: string; - containerId: string; - expectedDocumentIds: string[]; -}; - -export const documentTestCases: ContainerTestCase[] = [ - { - name: "System Partition Key", - databaseId: "e2etests-sql-readonly", - containerId: "systemPartitionKey", - expectedDocumentIds: ["systempartition"], - }, - { - name: "Single Partition Key", - databaseId: "e2etests-sql-readonly", - containerId: "singlePartitionKey", - expectedDocumentIds: [ - "singlePartitionKey", - "singlePartitionKey_empty_string", - "singlePartitionKey_null", - "singlePartitionKey_missing", - ], - }, - { - name: "Single Nested Partition Key", - databaseId: "e2etests-sql-readonly", - containerId: "singleNestedPartitionKey", - expectedDocumentIds: [ - "singlePartitionKey_nested", - "singlePartitionKey_nested_empty_string", - "singlePartitionKey_nested_null", - "singlePartitionKey_nested_missing", - ], - }, - { - name: "2-Level Hierarchical Partition Key", - databaseId: "e2etests-sql-readonly", - containerId: "twoLevelPartitionKey", - expectedDocumentIds: [ - "twoLevelPartitionKey_value_empty", - "twoLevelPartitionKey_value_null", - "twoLevelPartitionKey_value_missing", - "twoLevelPartitionKey_empty_null", - "twoLevelPartitionKey_null_missing", - "twoLevelPartitionKey_missing_value", - ], - }, - { - name: "2-Level Hierarchical Nested Partition Key", - databaseId: "e2etests-sql-readonly", - containerId: "twoLevelNestedPartitionKey", - expectedDocumentIds: [ - "twoLevelNestedPartitionKey_nested_value_empty", - "twoLevelNestedPartitionKey_nested_null_missing", - "twoLevelNestedPartitionKey_nested_missing_value", - ], - }, - { - name: "3-Level Hierarchical Partition Key", - databaseId: "e2etests-sql-readonly", - containerId: "threeLevelPartitionKey", - expectedDocumentIds: [ - "threeLevelPartitionKey_value_empty_null", - "threeLevelPartitionKey_value_null_missing", - "threeLevelPartitionKey_value_missing_null", - "threeLevelPartitionKey_null_empty_value", - "threeLevelPartitionKey_missing_value_value", - "threeLevelPartitionKey_empty_value_missing", - ], - }, - { - name: "3-Level Nested Hierarchical Partition Key", - databaseId: "e2etests-sql-readonly", - containerId: "threeLevelNestedPartitionKey", - expectedDocumentIds: [ - "threeLevelNestedPartitionKey_nested_empty_value_null", - "threeLevelNestedPartitionKey_nested_null_value_missing", - "threeLevelNestedPartitionKey_nested_missing_value_null", - "threeLevelNestedPartitionKey_nested_null_empty_missing", - "threeLevelNestedPartitionKey_nested_value_missing_empty", - "threeLevelNestedPartitionKey_nested_missing_null_empty", - "threeLevelNestedPartitionKey_nested_empty_null_value", - "threeLevelNestedPartitionKey_nested_value_null_empty", - ], - }, -]; From 2cff0fc3ff1206a28c4e9e6c802ab5c8e3861286 Mon Sep 17 00:00:00 2001 From: asier-isayas Date: Fri, 11 Apr 2025 10:39:32 -0400 Subject: [PATCH 07/87] Global Secondary Index (#2071) * add Materialized Views feature flag * fetch MV properties from RP API and capture them in our data models * AddMaterializedViewPanel * undefined check * subpartition keys * Partition Key, Throughput, Unique Keys * All views associated with a container (#2063) and Materialized View Target Container (#2065) Identified Source container and Target container Created tabs in Scale and Settings respectively Changed the Icon of target container * Add MV Panel * format * format * styling * add tests * tests * test files (#2074) Co-authored-by: nishthaAhujaa * fix type error * fix tests * merge conflict * Panel Integration (#2075) * integrated panel * edited header text --------- Co-authored-by: nishthaAhujaa Co-authored-by: Asier Isayas * updated tests (#2077) Co-authored-by: nishthaAhujaa * fix tests * update treeNodeUtil test snap * update settings component test snap * fixed source container in global "New Materialized View" * source container check (#2079) Co-authored-by: nishthaAhujaa * renamed Materialized Views to Global Secondary Index * more renaming * fix import * fix typo * disable materialized views for Fabric * updated input validation --------- Co-authored-by: Asier Isayas Co-authored-by: Nishtha Ahuja <45535788+nishthaAhujaa@users.noreply.github.com> Co-authored-by: nishthaAhujaa --- src/Common/Constants.ts | 3 + src/Common/DatabaseAccountUtility.ts | 7 + .../dataAccess/createMaterializedView.ts | 74 +++ src/Common/dataAccess/readCollections.ts | 9 +- src/Contracts/DataModels.ts | 28 +- src/Contracts/ViewModels.ts | 2 + src/Explorer/ContextMenuButtonFactory.tsx | 24 + .../Controls/Settings/SettingsComponent.tsx | 19 + .../GlobalSecondaryIndexComponent.test.tsx | 46 ++ .../GlobalSecondaryIndexComponent.tsx | 41 ++ ...obalSecondaryIndexSourceComponent.test.tsx | 42 ++ .../GlobalSecondaryIndexSourceComponent.tsx | 114 +++++ ...obalSecondaryIndexTargetComponent.test.tsx | 32 ++ .../GlobalSecondaryIndexTargetComponent.tsx | 45 ++ .../Controls/Settings/SettingsUtils.tsx | 3 + src/Explorer/Controls/Settings/TestUtils.tsx | 9 + .../SettingsComponent.test.tsx.snap | 101 ++++ src/Explorer/Explorer.tsx | 2 +- .../AddCollectionPanel.test.tsx | 2 +- .../AddCollectionPanel.tsx | 280 +++--------- .../AddCollectionPanelUtility.tsx | 217 +++++++++ .../AddCollectionPanel.test.tsx.snap | 0 .../AddGlobalSecondaryIndexPanel.test.tsx | 28 ++ .../AddGlobalSecondaryIndexPanel.tsx | 431 ++++++++++++++++++ .../AddGlobalSecondaryIndexPanelStyles.ts | 15 + .../Components/AdvancedComponent.tsx | 54 +++ .../Components/AnalyticalStoreComponent.tsx | 99 ++++ .../Components/FullTextSearchComponent.tsx | 45 ++ .../Components/PartitionKeyComponent.tsx | 132 ++++++ .../Components/ThroughputComponent.tsx | 71 +++ .../Components/UniqueKeysComponent.tsx | 78 ++++ .../Components/VectorSearchComponent.tsx | 58 +++ ...AddGlobalSecondaryIndexPanel.test.tsx.snap | 190 ++++++++ src/Explorer/QueryCopilot/CopilotCarousel.tsx | 2 +- src/Explorer/Sidebar.tsx | 25 + src/Explorer/Tree/Collection.ts | 4 + src/Explorer/Tree/Database.tsx | 2 +- src/Explorer/Tree/ResourceTree.tsx | 3 +- .../__snapshots__/treeNodeUtil.test.ts.snap | 50 +- src/Explorer/Tree/treeNodeUtil.test.ts | 10 + src/Explorer/Tree/treeNodeUtil.tsx | 9 +- src/Shared/Telemetry/TelemetryConstants.ts | 2 + 42 files changed, 2145 insertions(+), 263 deletions(-) create mode 100644 src/Common/dataAccess/createMaterializedView.ts create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexComponent.test.tsx create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexComponent.tsx create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexSourceComponent.test.tsx create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexSourceComponent.tsx create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexTargetComponent.test.tsx create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexTargetComponent.tsx rename src/Explorer/Panes/{ => AddCollectionPanel}/AddCollectionPanel.test.tsx (90%) rename src/Explorer/Panes/{ => AddCollectionPanel}/AddCollectionPanel.tsx (87%) create mode 100644 src/Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility.tsx rename src/Explorer/Panes/{ => AddCollectionPanel}/__snapshots__/AddCollectionPanel.test.tsx.snap (100%) create mode 100644 src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel.test.tsx create mode 100644 src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel.tsx create mode 100644 src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanelStyles.ts create mode 100644 src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AdvancedComponent.tsx create mode 100644 src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AnalyticalStoreComponent.tsx create mode 100644 src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/FullTextSearchComponent.tsx create mode 100644 src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/PartitionKeyComponent.tsx create mode 100644 src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent.tsx create mode 100644 src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent.tsx create mode 100644 src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/VectorSearchComponent.tsx create mode 100644 src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 94ca16c27..37243de72 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -530,6 +530,9 @@ export class ariaLabelForLearnMoreLink { public static readonly AzureSynapseLink = "Learn more about Azure Synapse Link."; } +export class GlobalSecondaryIndexLabels { + public static readonly NewGlobalSecondaryIndex: string = "New Global Secondary Index"; +} export class FeedbackLabels { public static readonly provideFeedback: string = "Provide feedback"; } diff --git a/src/Common/DatabaseAccountUtility.ts b/src/Common/DatabaseAccountUtility.ts index c72d3baf6..50ec0064a 100644 --- a/src/Common/DatabaseAccountUtility.ts +++ b/src/Common/DatabaseAccountUtility.ts @@ -1,5 +1,6 @@ import { TagNames, WorkloadType } from "Common/Constants"; import { Tags } from "Contracts/DataModels"; +import { isFabric } from "Platform/Fabric/FabricUtil"; import { userContext } from "../UserContext"; function isVirtualNetworkFilterEnabled() { @@ -26,3 +27,9 @@ export function getWorkloadType(): WorkloadType { } return workloadType; } + +export function isGlobalSecondaryIndexEnabled(): boolean { + return ( + !isFabric() && userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews + ); +} diff --git a/src/Common/dataAccess/createMaterializedView.ts b/src/Common/dataAccess/createMaterializedView.ts new file mode 100644 index 000000000..659da9c14 --- /dev/null +++ b/src/Common/dataAccess/createMaterializedView.ts @@ -0,0 +1,74 @@ +import { constructRpOptions } from "Common/dataAccess/createCollection"; +import { handleError } from "Common/ErrorHandlingUtils"; +import { Collection, CreateMaterializedViewsParams as CreateGlobalSecondaryIndexParams } from "Contracts/DataModels"; +import { userContext } from "UserContext"; +import { createUpdateSqlContainer } from "Utils/arm/generatedClients/cosmos/sqlResources"; +import { + CreateUpdateOptions, + SqlContainerResource, + SqlDatabaseCreateUpdateParameters, +} from "Utils/arm/generatedClients/cosmos/types"; +import { logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils"; + +export const createGlobalSecondaryIndex = async (params: CreateGlobalSecondaryIndexParams): Promise => { + const clearMessage = logConsoleProgress( + `Creating a new global secondary index ${params.materializedViewId} for database ${params.databaseId}`, + ); + + const options: CreateUpdateOptions = constructRpOptions(params); + + const resource: SqlContainerResource = { + id: params.materializedViewId, + }; + if (params.materializedViewDefinition) { + resource.materializedViewDefinition = params.materializedViewDefinition; + } + if (params.analyticalStorageTtl) { + resource.analyticalStorageTtl = params.analyticalStorageTtl; + } + if (params.indexingPolicy) { + resource.indexingPolicy = params.indexingPolicy; + } + if (params.partitionKey) { + resource.partitionKey = params.partitionKey; + } + if (params.uniqueKeyPolicy) { + resource.uniqueKeyPolicy = params.uniqueKeyPolicy; + } + if (params.vectorEmbeddingPolicy) { + resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy; + } + if (params.fullTextPolicy) { + resource.fullTextPolicy = params.fullTextPolicy; + } + + const rpPayload: SqlDatabaseCreateUpdateParameters = { + properties: { + resource, + options, + }, + }; + + try { + const createResponse = await createUpdateSqlContainer( + userContext.subscriptionId, + userContext.resourceGroup, + userContext.databaseAccount.name, + params.databaseId, + params.materializedViewId, + rpPayload, + ); + logConsoleInfo(`Successfully created global secondary index ${params.materializedViewId}`); + + return createResponse && (createResponse.properties.resource as Collection); + } catch (error) { + handleError( + error, + "CreateGlobalSecondaryIndex", + `Error while creating global secondary index ${params.materializedViewId}`, + ); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Common/dataAccess/readCollections.ts b/src/Common/dataAccess/readCollections.ts index ecb67c876..39e241cda 100644 --- a/src/Common/dataAccess/readCollections.ts +++ b/src/Common/dataAccess/readCollections.ts @@ -126,5 +126,12 @@ async function readCollectionsWithARM(databaseId: string): Promise collection.properties?.resource as DataModels.Collection); + // TO DO: Remove when we get RP API Spec with materializedViews + /* eslint-disable @typescript-eslint/no-explicit-any */ + return rpResponse?.value?.map((collection: any) => { + const collectionDataModel: DataModels.Collection = collection.properties?.resource as DataModels.Collection; + collectionDataModel.materializedViews = collection.properties?.resource?.materializedViews; + collectionDataModel.materializedViewDefinition = collection.properties?.resource?.materializedViewDefinition; + return collectionDataModel; + }); } diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index 3b3ab5027..58e412b76 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -32,6 +32,7 @@ export interface DatabaseAccountExtendedProperties { writeLocations?: DatabaseAccountResponseLocation[]; enableFreeTier?: boolean; enableAnalyticalStorage?: boolean; + enableMaterializedViews?: boolean; isVirtualNetworkFilterEnabled?: boolean; ipRules?: IpRule[]; privateEndpointConnections?: unknown[]; @@ -164,6 +165,8 @@ export interface Collection extends Resource { schema?: ISchema; requestSchema?: () => void; computedProperties?: ComputedProperties; + materializedViews?: MaterializedView[]; + materializedViewDefinition?: MaterializedViewDefinition; } export interface CollectionsWithPagination { @@ -223,6 +226,17 @@ export interface ComputedProperty { export type ComputedProperties = ComputedProperty[]; +export interface MaterializedView { + id: string; + _rid: string; +} + +export interface MaterializedViewDefinition { + definition: string; + sourceCollectionId: string; + sourceCollectionRid?: string; +} + export interface PartitionKey { paths: string[]; kind: "Hash" | "Range" | "MultiHash"; @@ -345,9 +359,7 @@ export interface CreateDatabaseParams { offerThroughput?: number; } -export interface CreateCollectionParams { - createNewDatabase: boolean; - collectionId: string; +export interface CreateCollectionParamsBase { databaseId: string; databaseLevelThroughput: boolean; offerThroughput?: number; @@ -361,6 +373,16 @@ export interface CreateCollectionParams { fullTextPolicy?: FullTextPolicy; } +export interface CreateCollectionParams extends CreateCollectionParamsBase { + createNewDatabase: boolean; + collectionId: string; +} + +export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase { + materializedViewId: string; + materializedViewDefinition: MaterializedViewDefinition; +} + export interface VectorEmbeddingPolicy { vectorEmbeddings: VectorEmbedding[]; } diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 83afa9ddb..a66d83b86 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -143,6 +143,8 @@ export interface Collection extends CollectionBase { geospatialConfig: ko.Observable; documentIds: ko.ObservableArray; computedProperties: ko.Observable; + materializedViews: ko.Observable; + materializedViewDefinition: ko.Observable; cassandraKeys: CassandraTableKeys; cassandraSchema: CassandraTableKey[]; diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx index 8108cbbb2..3cb4c7a80 100644 --- a/src/Explorer/ContextMenuButtonFactory.tsx +++ b/src/Explorer/ContextMenuButtonFactory.tsx @@ -1,5 +1,11 @@ +import { GlobalSecondaryIndexLabels } from "Common/Constants"; +import { isGlobalSecondaryIndexEnabled } from "Common/DatabaseAccountUtility"; import { configContext, Platform } from "ConfigContext"; import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; +import { + AddGlobalSecondaryIndexPanel, + AddGlobalSecondaryIndexPanelProps, +} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel"; import { useDatabases } from "Explorer/useDatabases"; import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil"; import { Action } from "Shared/Telemetry/TelemetryConstants"; @@ -164,6 +170,24 @@ export const createCollectionContextMenuButton = ( }); } + if (isGlobalSecondaryIndexEnabled() && !selectedCollection.materializedViewDefinition()) { + items.push({ + label: GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex, + onClick: () => { + const addMaterializedViewPanelProps: AddGlobalSecondaryIndexPanelProps = { + explorer: container, + sourceContainer: selectedCollection, + }; + useSidePanel + .getState() + .openSidePanel( + GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex, + , + ); + }, + }); + } + return items; }; diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 720bef874..beec14495 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -44,6 +44,10 @@ import { ConflictResolutionComponent, ConflictResolutionComponentProps, } from "./SettingsSubComponents/ConflictResolutionComponent"; +import { + GlobalSecondaryIndexComponent, + GlobalSecondaryIndexComponentProps, +} from "./SettingsSubComponents/GlobalSecondaryIndexComponent"; import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent"; import { MongoIndexingPolicyComponent, @@ -162,6 +166,7 @@ export class SettingsComponent extends React.Component, + }); + } + const pivotProps: IPivotProps = { onLinkClick: this.onPivotChange, selectedKey: SettingsV2TabTypes[this.state.selectedTab], diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexComponent.test.tsx new file mode 100644 index 000000000..ac290f93c --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexComponent.test.tsx @@ -0,0 +1,46 @@ +import { shallow } from "enzyme"; +import React from "react"; +import { collection, container } from "../TestUtils"; +import { GlobalSecondaryIndexComponent } from "./GlobalSecondaryIndexComponent"; +import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent"; +import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent"; + +describe("GlobalSecondaryIndexComponent", () => { + let testCollection: typeof collection; + let testExplorer: typeof container; + + beforeEach(() => { + testCollection = { ...collection }; + }); + + it("renders only the source component when materializedViewDefinition is missing", () => { + testCollection.materializedViews([ + { id: "view1", _rid: "rid1" }, + { id: "view2", _rid: "rid2" }, + ]); + testCollection.materializedViewDefinition(null); + const wrapper = shallow(); + expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(true); + expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(false); + }); + + it("renders only the target component when materializedViews is missing", () => { + testCollection.materializedViews(null); + testCollection.materializedViewDefinition({ + definition: "SELECT * FROM c WHERE c.id = 1", + sourceCollectionId: "source1", + sourceCollectionRid: "rid123", + }); + const wrapper = shallow(); + expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(false); + expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(true); + }); + + it("renders neither component when both are missing", () => { + testCollection.materializedViews(null); + testCollection.materializedViewDefinition(null); + const wrapper = shallow(); + expect(wrapper.find(GlobalSecondaryIndexSourceComponent).exists()).toBe(false); + expect(wrapper.find(GlobalSecondaryIndexTargetComponent).exists()).toBe(false); + }); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexComponent.tsx new file mode 100644 index 000000000..66aa3313a --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexComponent.tsx @@ -0,0 +1,41 @@ +import { FontIcon, Link, Stack, Text } from "@fluentui/react"; +import Explorer from "Explorer/Explorer"; +import React from "react"; +import * as ViewModels from "../../../../Contracts/ViewModels"; +import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent"; +import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent"; + +export interface GlobalSecondaryIndexComponentProps { + collection: ViewModels.Collection; + explorer: Explorer; +} + +export const GlobalSecondaryIndexComponent: React.FC = ({ + collection, + explorer, +}) => { + const isTargetContainer = !!collection?.materializedViewDefinition(); + const isSourceContainer = !!collection?.materializedViews(); + + return ( + + + {isSourceContainer && ( + This container has the following indexes defined for it. + )} + + + Learn more + + {" "} + about how to define global secondary indexes and how to use them. + + + {isSourceContainer && } + {isTargetContainer && } + + ); +}; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexSourceComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexSourceComponent.test.tsx new file mode 100644 index 000000000..30ac800b9 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexSourceComponent.test.tsx @@ -0,0 +1,42 @@ +import { PrimaryButton } from "@fluentui/react"; +import { shallow } from "enzyme"; +import React from "react"; +import { collection, container } from "../TestUtils"; +import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent"; + +describe("GlobalSecondaryIndexSourceComponent", () => { + let testCollection: typeof collection; + let testExplorer: typeof container; + + beforeEach(() => { + testCollection = { ...collection }; + }); + + it("renders without crashing", () => { + const wrapper = shallow( + , + ); + expect(wrapper.exists()).toBe(true); + }); + + it("renders the PrimaryButton", () => { + const wrapper = shallow( + , + ); + expect(wrapper.find(PrimaryButton).exists()).toBe(true); + }); + + it("updates when new global secondary indexes are provided", () => { + const wrapper = shallow( + , + ); + + // Simulating an update by modifying the observable directly + testCollection.materializedViews([{ id: "view3", _rid: "rid3" }]); + + wrapper.setProps({ collection: testCollection }); + wrapper.update(); + + expect(wrapper.find(PrimaryButton).exists()).toBe(true); + }); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexSourceComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexSourceComponent.tsx new file mode 100644 index 000000000..aa0a0edae --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexSourceComponent.tsx @@ -0,0 +1,114 @@ +import { PrimaryButton } from "@fluentui/react"; +import { GlobalSecondaryIndexLabels } from "Common/Constants"; +import { MaterializedView } from "Contracts/DataModels"; +import Explorer from "Explorer/Explorer"; +import { loadMonaco } from "Explorer/LazyMonaco"; +import { AddGlobalSecondaryIndexPanel } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel"; +import { useDatabases } from "Explorer/useDatabases"; +import { useSidePanel } from "hooks/useSidePanel"; +import * as monaco from "monaco-editor"; +import React, { useEffect, useRef } from "react"; +import * as ViewModels from "../../../../Contracts/ViewModels"; + +export interface GlobalSecondaryIndexSourceComponentProps { + collection: ViewModels.Collection; + explorer: Explorer; +} + +export const GlobalSecondaryIndexSourceComponent: React.FC = ({ + collection, + explorer, +}) => { + const editorContainerRef = useRef(null); + const editorRef = useRef(null); + + const globalSecondaryIndexes: MaterializedView[] = collection?.materializedViews() ?? []; + + // Helper function to fetch the definition and partition key of targetContainer by traversing through all collections and matching id from MaterializedView[] with collection id. + const getViewDetails = (viewId: string): { definition: string; partitionKey: string[] } => { + let definition = ""; + let partitionKey: string[] = []; + + useDatabases.getState().databases.find((database) => { + const collection = database.collections().find((collection) => collection.id() === viewId); + if (collection) { + const globalSecondaryIndexDefinition = collection.materializedViewDefinition(); + globalSecondaryIndexDefinition && (definition = globalSecondaryIndexDefinition.definition); + collection.partitionKey?.paths && (partitionKey = collection.partitionKey.paths); + } + }); + + return { definition, partitionKey }; + }; + + //JSON value for the editor using the fetched id and definitions. + const jsonValue = JSON.stringify( + globalSecondaryIndexes.map((view) => { + const { definition, partitionKey } = getViewDetails(view.id); + return { + name: view.id, + partitionKey: partitionKey.join(", "), + definition, + }; + }), + null, + 2, + ); + + // Initialize Monaco editor with the computed JSON value. + useEffect(() => { + let disposed = false; + const initMonaco = async () => { + const monacoInstance = await loadMonaco(); + if (disposed || !editorContainerRef.current) { + return; + } + + editorRef.current = monacoInstance.editor.create(editorContainerRef.current, { + value: jsonValue, + language: "json", + ariaLabel: "Global Secondary Index JSON", + readOnly: true, + }); + }; + + initMonaco(); + return () => { + disposed = true; + editorRef.current?.dispose(); + }; + }, [jsonValue]); + + // Update the editor when the jsonValue changes. + useEffect(() => { + if (editorRef.current) { + editorRef.current.setValue(jsonValue); + } + }, [jsonValue]); + + return ( +
    +
    + + useSidePanel + .getState() + .openSidePanel( + GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex, + , + ) + } + /> +
    + ); +}; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexTargetComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexTargetComponent.test.tsx new file mode 100644 index 000000000..6296cdab7 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexTargetComponent.test.tsx @@ -0,0 +1,32 @@ +import { Text } from "@fluentui/react"; +import { Collection } from "Contracts/ViewModels"; +import { shallow } from "enzyme"; +import React from "react"; +import { collection } from "../TestUtils"; +import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent"; + +describe("GlobalSecondaryIndexTargetComponent", () => { + let testCollection: Collection; + + beforeEach(() => { + testCollection = { + ...collection, + materializedViewDefinition: collection.materializedViewDefinition, + }; + }); + + it("renders without crashing", () => { + const wrapper = shallow(); + expect(wrapper.exists()).toBe(true); + }); + + it("displays the source container ID", () => { + const wrapper = shallow(); + expect(wrapper.find(Text).at(2).dive().text()).toBe("source1"); + }); + + it("displays the global secondary index definition", () => { + const wrapper = shallow(); + expect(wrapper.find(Text).at(4).dive().text()).toBe("SELECT * FROM c WHERE c.id = 1"); + }); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexTargetComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexTargetComponent.tsx new file mode 100644 index 000000000..8fa1171e8 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/GlobalSecondaryIndexTargetComponent.tsx @@ -0,0 +1,45 @@ +import { Stack, Text } from "@fluentui/react"; +import * as React from "react"; +import * as ViewModels from "../../../../Contracts/ViewModels"; + +export interface GlobalSecondaryIndexTargetComponentProps { + collection: ViewModels.Collection; +} + +export const GlobalSecondaryIndexTargetComponent: React.FC = ({ + collection, +}) => { + const globalSecondaryIndexDefinition = collection?.materializedViewDefinition(); + + const textHeadingStyle = { + root: { fontWeight: "600", fontSize: 16 }, + }; + + const valueBoxStyle = { + root: { + backgroundColor: "#f3f3f3", + padding: "5px 10px", + borderRadius: "4px", + }, + }; + + return ( + + Global Secondary Index Settings + + + Source container + + {globalSecondaryIndexDefinition?.sourceCollectionId} + + + + + Global secondary index definition + + {globalSecondaryIndexDefinition?.definition} + + + + ); +}; diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index 900ad6ab0..448b59370 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -57,6 +57,7 @@ export enum SettingsV2TabTypes { ComputedPropertiesTab, ContainerVectorPolicyTab, ThroughputBucketsTab, + GlobalSecondaryIndexTab, } export enum ContainerPolicyTabTypes { @@ -171,6 +172,8 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => { return "Container Policies"; case SettingsV2TabTypes.ThroughputBucketsTab: return "Throughput Buckets"; + case SettingsV2TabTypes.GlobalSecondaryIndexTab: + return "Global Secondary Index (Preview)"; default: throw new Error(`Unknown tab ${tab}`); } diff --git a/src/Explorer/Controls/Settings/TestUtils.tsx b/src/Explorer/Controls/Settings/TestUtils.tsx index c158e5cba..71d939584 100644 --- a/src/Explorer/Controls/Settings/TestUtils.tsx +++ b/src/Explorer/Controls/Settings/TestUtils.tsx @@ -48,6 +48,15 @@ export const collection = { ]), vectorEmbeddingPolicy: ko.observable({} as DataModels.VectorEmbeddingPolicy), fullTextPolicy: ko.observable({} as DataModels.FullTextPolicy), + materializedViews: ko.observable([ + { id: "view1", _rid: "rid1" }, + { id: "view2", _rid: "rid2" }, + ]), + materializedViewDefinition: ko.observable({ + definition: "SELECT * FROM c WHERE c.id = 1", + sourceCollectionId: "source1", + sourceCollectionRid: "rid123", + }), readSettings: () => { return; }, diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index ea6fe2864..34f6dec6d 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -60,6 +60,8 @@ exports[`SettingsComponent renders 1`] = ` "getDatabase": [Function], "id": [Function], "indexingPolicy": [Function], + "materializedViewDefinition": [Function], + "materializedViews": [Function], "offer": [Function], "partitionKey": { "kind": "hash", @@ -139,6 +141,8 @@ exports[`SettingsComponent renders 1`] = ` "getDatabase": [Function], "id": [Function], "indexingPolicy": [Function], + "materializedViewDefinition": [Function], + "materializedViews": [Function], "offer": [Function], "partitionKey": { "kind": "hash", @@ -258,6 +262,8 @@ exports[`SettingsComponent renders 1`] = ` "getDatabase": [Function], "id": [Function], "indexingPolicy": [Function], + "materializedViewDefinition": [Function], + "materializedViews": [Function], "offer": [Function], "partitionKey": { "kind": "hash", @@ -336,6 +342,101 @@ exports[`SettingsComponent renders 1`] = ` shouldDiscardComputedProperties={false} /> + + +
    diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index ace0aaffe..e62bfe5b3 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -55,7 +55,7 @@ import type NotebookManager from "./Notebook/NotebookManager"; import { NotebookPaneContent } from "./Notebook/NotebookManager"; import { NotebookUtil } from "./Notebook/NotebookUtil"; import { useNotebook } from "./Notebook/useNotebook"; -import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; +import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; diff --git a/src/Explorer/Panes/AddCollectionPanel.test.tsx b/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.test.tsx similarity index 90% rename from src/Explorer/Panes/AddCollectionPanel.test.tsx rename to src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.test.tsx index 0e87c61de..f075eb828 100644 --- a/src/Explorer/Panes/AddCollectionPanel.test.tsx +++ b/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.test.tsx @@ -1,6 +1,6 @@ import { shallow } from "enzyme"; import React from "react"; -import Explorer from "../Explorer"; +import Explorer from "../../Explorer"; import { AddCollectionPanel } from "./AddCollectionPanel"; const props = { diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx similarity index 87% rename from src/Explorer/Panes/AddCollectionPanel.tsx rename to src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx index 0256418d4..c92eda06b 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx @@ -21,11 +21,25 @@ import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { configContext, Platform } from "ConfigContext"; import * as DataModels from "Contracts/DataModels"; -import { - FullTextPoliciesComponent, - getFullTextLanguageOptions, -} from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent"; +import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent"; import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent"; +import { + AllPropertiesIndexed, + AnalyticalStorageContent, + ContainerVectorPolicyTooltipContent, + FullTextPolicyDefault, + getPartitionKey, + getPartitionKeyName, + getPartitionKeyPlaceHolder, + getPartitionKeyTooltipText, + isFreeTierAccount, + isSynapseLinkEnabled, + parseUniqueKeys, + scrollToSection, + SharedDatabaseDefault, + shouldShowAnalyticalStoreOptions, + UniqueKeysHeader, +} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; import { useSidePanel } from "hooks/useSidePanel"; import { useTeachingBubble } from "hooks/useTeachingBubble"; import { isFabricNative } from "Platform/Fabric/FabricUtil"; @@ -43,15 +57,14 @@ import { } from "Utils/CapabilityUtils"; import { getUpsellMessage } from "Utils/PricingUtils"; import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils"; -import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; -import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; -import "../Controls/ThroughputInput/ThroughputInput.less"; -import { ContainerSampleGenerator } from "../DataSamples/ContainerSampleGenerator"; -import Explorer from "../Explorer"; -import { useDatabases } from "../useDatabases"; -import { PanelFooterComponent } from "./PanelFooterComponent"; -import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent"; -import { PanelLoadingScreen } from "./PanelLoadingScreen"; +import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent"; +import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; +import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator"; +import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; +import { PanelFooterComponent } from "../PanelFooterComponent"; +import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent"; +import { PanelLoadingScreen } from "../PanelLoadingScreen"; export interface AddCollectionPanelProps { explorer: Explorer; @@ -59,40 +72,6 @@ export interface AddCollectionPanelProps { isQuickstart?: boolean; } -const SharedDatabaseDefault: DataModels.IndexingPolicy = { - indexingMode: "consistent", - automatic: true, - includedPaths: [], - excludedPaths: [ - { - path: "/*", - }, - ], -}; - -export const AllPropertiesIndexed: DataModels.IndexingPolicy = { - indexingMode: "consistent", - automatic: true, - includedPaths: [ - { - path: "/*", - indexes: [ - { - kind: "Range", - dataType: "Number", - precision: -1, - }, - { - kind: "Range", - dataType: "String", - precision: -1, - }, - ], - }, - ], - excludedPaths: [], -}; - export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = { vectorEmbeddings: [], }; @@ -145,7 +124,7 @@ export class AddCollectionPanel extends React.Component )} - {!this.state.errorMessage && this.isFreeTierAccount() && ( + {!this.state.errorMessage && isFreeTierAccount() && ( (this.newDatabaseThroughput = throughput)} setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)} @@ -580,17 +559,14 @@ export class AddCollectionPanel extends React.Component - {this.getPartitionKeyName()} + {getPartitionKeyName()} - + @@ -604,8 +580,8 @@ export class AddCollectionPanel extends React.Component 0 ? 1 : 0} className="panelTextField" autoComplete="off" - placeholder={this.getPartitionKeyPlaceHolder(index)} - aria-label={this.getPartitionKeyName()} + placeholder={getPartitionKeyPlaceHolder(index)} + aria-label={getPartitionKeyName()} pattern={".*"} title={""} value={subPartitionKey} @@ -735,10 +711,10 @@ export class AddCollectionPanel extends React.Component (this.collectionThroughput = throughput)} setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)} @@ -753,27 +729,7 @@ export class AddCollectionPanel extends React.Component - - - Unique keys - - - - - - + {UniqueKeysHeader()} {this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => { return ( @@ -821,10 +777,10 @@ export class AddCollectionPanel extends React.Component )} - {this.shouldShowAnalyticalStoreOptions() && ( + {shouldShowAnalyticalStoreOptions() && ( - {this.getAnalyticalStorageContent()} + {AnalyticalStorageContent()} @@ -832,7 +788,7 @@ export class AddCollectionPanel extends React.Component - {!this.isSynapseLinkEnabled() && ( + {!isSynapseLinkEnabled() && ( Azure Synapse Link is required for creating an analytical store{" "} @@ -891,9 +847,9 @@ export class AddCollectionPanel extends React.Component { - this.scrollToSection("collapsibleVectorPolicySectionContent"); + scrollToSection("collapsibleVectorPolicySectionContent"); }} - tooltipContent={this.getContainerVectorPolicyTooltipContent()} + tooltipContent={ContainerVectorPolicyTooltipContent()} > @@ -919,7 +875,7 @@ export class AddCollectionPanel extends React.Component { - this.scrollToSection("collapsibleFullTextPolicySectionContent"); + scrollToSection("collapsibleFullTextPolicySectionContent"); }} //TODO: uncomment when learn more text becomes available // tooltipContent={this.getContainerFullTextPolicyTooltipContent()} @@ -947,7 +903,7 @@ export class AddCollectionPanel extends React.Component { TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection); - this.scrollToSection("collapsibleAdvancedSectionContent"); + scrollToSection("collapsibleAdvancedSectionContent"); }} > @@ -1057,31 +1013,6 @@ export class AddCollectionPanel extends React.Component): void { if (event.target.checked && !this.state.createNewDatabase) { this.setState({ @@ -1169,48 +1100,12 @@ export class AddCollectionPanel extends React.Component - Enable analytical store capability to perform near real-time analytics on your operational data, without - impacting the performance of transactional workloads.{" "} - - Learn more - - - ); - } - - private getContainerVectorPolicyTooltipContent(): JSX.Element { - return ( - - Describe any properties in your data that contain vectors, so that they can be made available for similarity - queries.{" "} - - Learn more - - - ); - } - //TODO: uncomment when learn more text becomes available // private getContainerFullTextPolicyTooltipContent(): JSX.Element { // return ( @@ -1280,7 +1147,7 @@ export class AddCollectionPanel extends React.Component capability.name === Constants.CapabilityNames.EnableStorageAnalytics, - ); - } - private shouldShowVectorSearchParameters() { return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput()); } @@ -1402,11 +1236,11 @@ export class AddCollectionPanel extends React.Component + + Unique keys + + + + + + ); +} + +export function shouldShowAnalyticalStoreOptions(): boolean { + if (isFabricNative() || configContext.platform === Platform.Emulator) { + return false; + } + + switch (userContext.apiType) { + case "SQL": + case "Mongo": + return true; + default: + return false; + } +} + +export function AnalyticalStorageContent(): JSX.Element { + return ( + + Enable analytical store capability to perform near real-time analytics on your operational data, without impacting + the performance of transactional workloads.{" "} + + Learn more + + + ); +} + +export function isSynapseLinkEnabled(): boolean { + if (!userContext.databaseAccount) { + return false; + } + + const { properties } = userContext.databaseAccount; + if (!properties) { + return false; + } + + if (properties.enableAnalyticalStorage) { + return true; + } + + return properties.capabilities?.some( + (capability) => capability.name === Constants.CapabilityNames.EnableStorageAnalytics, + ); +} + +export function scrollToSection(id: string): void { + document.getElementById(id)?.scrollIntoView(); +} + +export function ContainerVectorPolicyTooltipContent(): JSX.Element { + return ( + + Describe any properties in your data that contain vectors, so that they can be made available for similarity + queries.{" "} + + Learn more + + + ); +} + +export function parseUniqueKeys(uniqueKeys: string[]): DataModels.UniqueKeyPolicy { + if (uniqueKeys?.length === 0) { + return undefined; + } + + const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = { uniqueKeys: [] }; + uniqueKeys.forEach((uniqueKey: string) => { + if (uniqueKey) { + const validPaths: string[] = uniqueKey.split(",")?.filter((path) => path?.length > 0); + const trimmedPaths: string[] = validPaths?.map((path) => path.trim()); + if (trimmedPaths?.length > 0) { + if (userContext.apiType === "Mongo") { + trimmedPaths.map((path) => { + const transformedPath = path.split(".").join("/"); + if (transformedPath[0] !== "/") { + return "/" + transformedPath; + } + return transformedPath; + }); + } + uniqueKeyPolicy.uniqueKeys.push({ paths: trimmedPaths }); + } + } + }); + + return uniqueKeyPolicy; +} + +export const SharedDatabaseDefault: DataModels.IndexingPolicy = { + indexingMode: "consistent", + automatic: true, + includedPaths: [], + excludedPaths: [ + { + path: "/*", + }, + ], +}; + +export const FullTextPolicyDefault: DataModels.FullTextPolicy = { + defaultLanguage: getFullTextLanguageOptions()[0].key as never, + fullTextPaths: [], +}; + +export const AllPropertiesIndexed: DataModels.IndexingPolicy = { + indexingMode: "consistent", + automatic: true, + includedPaths: [ + { + path: "/*", + indexes: [ + { + kind: "Range", + dataType: "Number", + precision: -1, + }, + { + kind: "Range", + dataType: "String", + precision: -1, + }, + ], + }, + ], + excludedPaths: [], +}; diff --git a/src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap b/src/Explorer/Panes/AddCollectionPanel/__snapshots__/AddCollectionPanel.test.tsx.snap similarity index 100% rename from src/Explorer/Panes/__snapshots__/AddCollectionPanel.test.tsx.snap rename to src/Explorer/Panes/AddCollectionPanel/__snapshots__/AddCollectionPanel.test.tsx.snap diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel.test.tsx b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel.test.tsx new file mode 100644 index 000000000..05eec133d --- /dev/null +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel.test.tsx @@ -0,0 +1,28 @@ +import { shallow, ShallowWrapper } from "enzyme"; +import Explorer from "Explorer/Explorer"; +import { + AddGlobalSecondaryIndexPanel, + AddGlobalSecondaryIndexPanelProps, +} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel"; +import React, { Component } from "react"; + +const props: AddGlobalSecondaryIndexPanelProps = { + explorer: new Explorer(), +}; + +describe("AddGlobalSecondaryIndexPanel", () => { + it("render default panel", () => { + const wrapper: ShallowWrapper = shallow( + , + ); + expect(wrapper).toMatchSnapshot(); + }); + + it("should render form", () => { + const wrapper: ShallowWrapper = shallow( + , + ); + const form = wrapper.find("form").first(); + expect(form).toBeDefined(); + }); +}); diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel.tsx b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel.tsx new file mode 100644 index 000000000..313c72b4c --- /dev/null +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel.tsx @@ -0,0 +1,431 @@ +import { + DirectionalHint, + Dropdown, + DropdownMenuItemType, + Icon, + IDropdownOption, + Link, + Separator, + Stack, + Text, + TooltipHost, +} from "@fluentui/react"; +import * as Constants from "Common/Constants"; +import { createGlobalSecondaryIndex } from "Common/dataAccess/createMaterializedView"; +import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; +import * as DataModels from "Contracts/DataModels"; +import { FullTextIndex, FullTextPolicy, VectorEmbedding, VectorIndex } from "Contracts/DataModels"; +import { Collection, Database } from "Contracts/ViewModels"; +import Explorer from "Explorer/Explorer"; +import { + AllPropertiesIndexed, + FullTextPolicyDefault, + getPartitionKey, + isSynapseLinkEnabled, + parseUniqueKeys, + scrollToSection, + shouldShowAnalyticalStoreOptions, +} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; +import { + chooseSourceContainerStyle, + chooseSourceContainerStyles, +} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanelStyles"; +import { AdvancedComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AdvancedComponent"; +import { AnalyticalStoreComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AnalyticalStoreComponent"; +import { FullTextSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/FullTextSearchComponent"; +import { PartitionKeyComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/PartitionKeyComponent"; +import { ThroughputComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent"; +import { UniqueKeysComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent"; +import { VectorSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/VectorSearchComponent"; +import { PanelFooterComponent } from "Explorer/Panes/PanelFooterComponent"; +import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent"; +import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen"; +import { useDatabases } from "Explorer/useDatabases"; +import { useSidePanel } from "hooks/useSidePanel"; +import React, { useEffect, useState } from "react"; +import { CollectionCreation } from "Shared/Constants"; +import { Action } from "Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; +import { userContext } from "UserContext"; +import { isFullTextSearchEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils"; +import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils"; + +export interface AddGlobalSecondaryIndexPanelProps { + explorer: Explorer; + sourceContainer?: Collection; +} +export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanelProps): JSX.Element => { + const { explorer, sourceContainer } = props; + + const [sourceContainerOptions, setSourceContainerOptions] = useState(); + const [selectedSourceContainer, setSelectedSourceContainer] = useState(sourceContainer); + const [globalSecondaryIndexId, setGlobalSecondaryIndexId] = useState(); + const [definition, setDefinition] = useState(); + const [partitionKey, setPartitionKey] = useState(getPartitionKey()); + const [subPartitionKeys, setSubPartitionKeys] = useState([]); + const [useHashV1, setUseHashV1] = useState(); + const [enableDedicatedThroughput, setEnabledDedicatedThroughput] = useState(); + const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState(); + const [uniqueKeys, setUniqueKeys] = useState([]); + const [enableAnalyticalStore, setEnableAnalyticalStore] = useState(); + const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState(); + const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState(); + const [vectorPolicyValidated, setVectorPolicyValidated] = useState(); + const [fullTextPolicy, setFullTextPolicy] = useState(FullTextPolicyDefault); + const [fullTextIndexes, setFullTextIndexes] = useState(); + const [fullTextPolicyValidated, setFullTextPolicyValidated] = useState(); + const [errorMessage, setErrorMessage] = useState(); + const [showErrorDetails, setShowErrorDetails] = useState(); + const [isExecuting, setIsExecuting] = useState(); + + useEffect(() => { + const sourceContainerOptions: IDropdownOption[] = []; + useDatabases.getState().databases.forEach((database: Database) => { + sourceContainerOptions.push({ + key: database.rid, + text: database.id(), + itemType: DropdownMenuItemType.Header, + }); + + database.collections().forEach((collection: Collection) => { + const isGlobalSecondaryIndex: boolean = !!collection.materializedViewDefinition(); + sourceContainerOptions.push({ + key: collection.rid, + text: collection.id(), + disabled: isGlobalSecondaryIndex, + ...(isGlobalSecondaryIndex && { + title: "This is a global secondary index.", + }), + data: collection, + }); + }); + }); + + setSourceContainerOptions(sourceContainerOptions); + }, []); + + useEffect(() => { + scrollToSection("panelContainer"); + }, [errorMessage]); + + let globalSecondaryIndexThroughput: number; + let isGlobalSecondaryIndexAutoscale: boolean; + let isCostAcknowledged: boolean; + + const globalSecondaryIndexThroughputOnChange = (globalSecondaryIndexThroughputValue: number): void => { + globalSecondaryIndexThroughput = globalSecondaryIndexThroughputValue; + }; + + const isGlobalSecondaryIndexAutoscaleOnChange = (isGlobalSecondaryIndexAutoscaleValue: boolean): void => { + isGlobalSecondaryIndexAutoscale = isGlobalSecondaryIndexAutoscaleValue; + }; + + const isCostAknowledgedOnChange = (isCostAcknowledgedValue: boolean): void => { + isCostAcknowledged = isCostAcknowledgedValue; + }; + + const isSelectedSourceContainerSharedThroughput = (): boolean => { + if (!selectedSourceContainer) { + return false; + } + + return !!selectedSourceContainer.getDatabase().offer(); + }; + + const showCollectionThroughputInput = (): boolean => { + if (isServerlessAccount()) { + return false; + } + + if (enableDedicatedThroughput) { + return true; + } + + return !!selectedSourceContainer && !isSelectedSourceContainerSharedThroughput(); + }; + + const showVectorSearchParameters = (): boolean => { + return isVectorSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput()); + }; + + const showFullTextSearchParameters = (): boolean => { + return isFullTextSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput()); + }; + + const getAnalyticalStorageTtl = (): number => { + if (!isSynapseLinkEnabled()) { + return undefined; + } + + if (!shouldShowAnalyticalStoreOptions()) { + return undefined; + } + + if (enableAnalyticalStore) { + // TODO: always default to 90 days once the backend hotfix is deployed + return userContext.features.ttl90Days + ? Constants.AnalyticalStorageTtl.Days90 + : Constants.AnalyticalStorageTtl.Infinite; + } + + return Constants.AnalyticalStorageTtl.Disabled; + }; + + const validateInputs = (): boolean => { + if (!selectedSourceContainer) { + setErrorMessage("Please select a source container"); + return false; + } + + if (globalSecondaryIndexThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) { + const errorMessage = isGlobalSecondaryIndexAutoscale + ? "Please acknowledge the estimated monthly spend." + : "Please acknowledge the estimated daily spend."; + setErrorMessage(errorMessage); + 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"); + return false; + } + + if (!fullTextPolicyValidated) { + setErrorMessage("Please fix errors in container full text search policy"); + return false; + } + } + + return true; + }; + + const submit = async (event?: React.FormEvent): Promise => { + event?.preventDefault(); + + if (!validateInputs()) { + return; + } + + const globalSecondaryIdTrimmed: string = globalSecondaryIndexId.trim(); + + const globalSecondaryIndexDefinition: DataModels.MaterializedViewDefinition = { + sourceCollectionId: selectedSourceContainer.id(), + definition: definition, + }; + + const partitionKeyTrimmed: string = partitionKey.trim(); + + const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(uniqueKeys); + const partitionKeyVersion = useHashV1 ? undefined : 2; + const partitionKeyPaths: DataModels.PartitionKey = partitionKeyTrimmed + ? { + paths: [ + partitionKeyTrimmed, + ...(userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? subPartitionKeys : []), + ], + kind: userContext.apiType === "SQL" && subPartitionKeys.length > 0 ? "MultiHash" : "Hash", + version: partitionKeyVersion, + } + : undefined; + + const indexingPolicy: DataModels.IndexingPolicy = AllPropertiesIndexed; + let vectorEmbeddingPolicyFinal: DataModels.VectorEmbeddingPolicy; + + if (showVectorSearchParameters()) { + indexingPolicy.vectorIndexes = vectorIndexingPolicy; + vectorEmbeddingPolicyFinal = { + vectorEmbeddings: vectorEmbeddingPolicy, + }; + } + + if (showFullTextSearchParameters()) { + indexingPolicy.fullTextIndexes = fullTextIndexes; + } + + const telemetryData: TelemetryProcessor.TelemetryData = { + database: { + id: selectedSourceContainer.databaseId, + shared: isSelectedSourceContainerSharedThroughput(), + }, + collection: { + id: globalSecondaryIdTrimmed, + throughput: globalSecondaryIndexThroughput, + isAutoscale: isGlobalSecondaryIndexAutoscale, + partitionKeyPaths, + uniqueKeyPolicy, + collectionWithDedicatedThroughput: enableDedicatedThroughput, + }, + subscriptionQuotaId: userContext.quotaId, + dataExplorerArea: Constants.Areas.ContextualPane, + }; + + const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, telemetryData); + const databaseLevelThroughput: boolean = isSelectedSourceContainerSharedThroughput() && !enableDedicatedThroughput; + + let offerThroughput: number; + let autoPilotMaxThroughput: number; + + if (!databaseLevelThroughput) { + if (isGlobalSecondaryIndexAutoscale) { + autoPilotMaxThroughput = globalSecondaryIndexThroughput; + } else { + offerThroughput = globalSecondaryIndexThroughput; + } + } + + const createGlobalSecondaryIndexParams: DataModels.CreateMaterializedViewsParams = { + materializedViewId: globalSecondaryIdTrimmed, + materializedViewDefinition: globalSecondaryIndexDefinition, + databaseId: selectedSourceContainer.databaseId, + databaseLevelThroughput: databaseLevelThroughput, + offerThroughput: offerThroughput, + autoPilotMaxThroughput: autoPilotMaxThroughput, + analyticalStorageTtl: getAnalyticalStorageTtl(), + indexingPolicy: indexingPolicy, + partitionKey: partitionKeyPaths, + uniqueKeyPolicy: uniqueKeyPolicy, + vectorEmbeddingPolicy: vectorEmbeddingPolicyFinal, + fullTextPolicy: fullTextPolicy, + }; + + setIsExecuting(true); + + try { + await createGlobalSecondaryIndex(createGlobalSecondaryIndexParams); + await explorer.refreshAllDatabases(); + TelemetryProcessor.traceSuccess(Action.CreateGlobalSecondaryIndex, telemetryData, startKey); + useSidePanel.getState().closeSidePanel(); + } catch (error) { + const errorMessage: string = getErrorMessage(error); + setErrorMessage(errorMessage); + setShowErrorDetails(true); + const failureTelemetryData = { ...telemetryData, error: errorMessage, errorStack: getErrorStack(error) }; + TelemetryProcessor.traceFailure(Action.CreateGlobalSecondaryIndex, failureTelemetryData, startKey); + } finally { + setIsExecuting(false); + } + }; + + return ( +
    + {errorMessage && ( + + )} +
    + + + + + Source container id + + + setSelectedSourceContainer(options.data as Collection)} + /> + + + + + Global secondary index container id + + + ) => setGlobalSecondaryIndexId(event.target.value)} + /> + + + + Global secondary index definition + + + Learn more about defining global secondary indexes. + + } + > + + + + ) => setDefinition(event.target.value)} + /> + + + + {shouldShowAnalyticalStoreOptions() && ( + + )} + {showVectorSearchParameters() && ( + + )} + {showFullTextSearchParameters() && ( + + )} + + +
    + + {isExecuting && } + + ); +}; diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanelStyles.ts b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanelStyles.ts new file mode 100644 index 000000000..cfb6da846 --- /dev/null +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanelStyles.ts @@ -0,0 +1,15 @@ +import { IDropdownStyleProps, IDropdownStyles, IStyleFunctionOrObject } from "@fluentui/react"; +import { CSSProperties } from "react"; + +export function chooseSourceContainerStyles(): IStyleFunctionOrObject { + return { + title: { height: 27, lineHeight: 27 }, + dropdownItem: { fontSize: 12 }, + dropdownItemDisabled: { fontSize: 12 }, + dropdownItemSelected: { fontSize: 12 }, + }; +} + +export function chooseSourceContainerStyle(): CSSProperties { + return { width: 300, fontSize: 12 }; +} diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AdvancedComponent.tsx b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AdvancedComponent.tsx new file mode 100644 index 000000000..17d1cb303 --- /dev/null +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AdvancedComponent.tsx @@ -0,0 +1,54 @@ +import { Checkbox, Icon, Link, Stack, Text } from "@fluentui/react"; +import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent"; +import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; +import React from "react"; +import { Action } from "Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; + +export interface AdvancedComponentProps { + useHashV1: boolean; + setUseHashV1: React.Dispatch>; + setSubPartitionKeys: React.Dispatch>; +} +export const AdvancedComponent = (props: AdvancedComponentProps): JSX.Element => { + const { useHashV1, setUseHashV1, setSubPartitionKeys } = props; + + const useHashV1CheckboxOnChange = (isChecked: boolean): void => { + setUseHashV1(isChecked); + setSubPartitionKeys([]); + }; + + return ( + { + TelemetryProcessor.traceOpen(Action.ExpandAddGlobalSecondaryIndexPaneAdvancedSection); + scrollToSection("collapsibleAdvancedSectionContent"); + }} + > + + , isChecked: boolean) => { + useHashV1CheckboxOnChange(isChecked); + }} + /> + + To ensure compatibility with older SDKs, the created + container will use a legacy partitioning scheme that supports partition key values of size only up to 101 + bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "} + + Learn more + + + + + ); +}; diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AnalyticalStoreComponent.tsx b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AnalyticalStoreComponent.tsx new file mode 100644 index 000000000..46c28c6d8 --- /dev/null +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/AnalyticalStoreComponent.tsx @@ -0,0 +1,99 @@ +import { DefaultButton, Link, Stack, Text } from "@fluentui/react"; +import * as Constants from "Common/Constants"; +import Explorer from "Explorer/Explorer"; +import { + AnalyticalStorageContent, + isSynapseLinkEnabled, +} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; +import React from "react"; +import { getCollectionName } from "Utils/APITypeUtils"; + +export interface AnalyticalStoreComponentProps { + explorer: Explorer; + enableAnalyticalStore: boolean; + setEnableAnalyticalStore: React.Dispatch>; +} +export const AnalyticalStoreComponent = (props: AnalyticalStoreComponentProps): JSX.Element => { + const { explorer, enableAnalyticalStore, setEnableAnalyticalStore } = props; + + const onEnableAnalyticalStoreRadioButtonChange = (checked: boolean): void => { + if (checked && !enableAnalyticalStore) { + setEnableAnalyticalStore(true); + } + }; + + const onDisableAnalyticalStoreRadioButtonnChange = (checked: boolean): void => { + if (checked && enableAnalyticalStore) { + setEnableAnalyticalStore(false); + } + }; + + return ( + + + {AnalyticalStorageContent()} + + + +
    + ) => { + onEnableAnalyticalStoreRadioButtonChange(event.target.checked); + }} + /> + On + + ) => { + onDisableAnalyticalStoreRadioButtonnChange(event.target.checked); + }} + /> + Off +
    +
    + + {!isSynapseLinkEnabled() && ( + + + Azure Synapse Link is required for creating an analytical store {getCollectionName().toLocaleLowerCase()}. + Enable Synapse Link for this Cosmos DB account.{" "} + + Learn more + + + explorer.openEnableSynapseLinkDialog()} + style={{ height: 27, width: 80 }} + styles={{ label: { fontSize: 12 } }} + /> + + )} +
    + ); +}; diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/FullTextSearchComponent.tsx b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/FullTextSearchComponent.tsx new file mode 100644 index 000000000..e02bce6ab --- /dev/null +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/FullTextSearchComponent.tsx @@ -0,0 +1,45 @@ +import { Stack } from "@fluentui/react"; +import { FullTextIndex, FullTextPolicy } from "Contracts/DataModels"; +import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent"; +import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent"; +import { scrollToSection } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; +import React from "react"; + +export interface FullTextSearchComponentProps { + fullTextPolicy: FullTextPolicy; + setFullTextPolicy: React.Dispatch>; + setFullTextIndexes: React.Dispatch>; + setFullTextPolicyValidated: React.Dispatch>; +} +export const FullTextSearchComponent = (props: FullTextSearchComponentProps): JSX.Element => { + const { fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated } = props; + + return ( + + { + scrollToSection("collapsibleFullTextPolicySectionContent"); + }} + > + + + { + setFullTextPolicy(fullTextPolicy); + setFullTextIndexes(fullTextIndexes); + setFullTextPolicyValidated(fullTextPolicyValidated); + }} + /> + + + + + ); +}; diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/PartitionKeyComponent.tsx b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/PartitionKeyComponent.tsx new file mode 100644 index 000000000..f8f01c24b --- /dev/null +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/PartitionKeyComponent.tsx @@ -0,0 +1,132 @@ +import { DefaultButton, DirectionalHint, Icon, IconButton, Link, Stack, Text, TooltipHost } from "@fluentui/react"; +import * as Constants from "Common/Constants"; +import { + getPartitionKeyName, + getPartitionKeyPlaceHolder, + getPartitionKeyTooltipText, +} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; +import React from "react"; + +export interface PartitionKeyComponentProps { + partitionKey?: string; + setPartitionKey: React.Dispatch>; + subPartitionKeys: string[]; + setSubPartitionKeys: React.Dispatch>; + useHashV1: boolean; +} + +export const PartitionKeyComponent = (props: PartitionKeyComponentProps): JSX.Element => { + const { partitionKey, setPartitionKey, subPartitionKeys, setSubPartitionKeys, useHashV1 } = props; + + const partitionKeyValueOnChange = (value: string): void => { + if (!partitionKey && !value.startsWith("/")) { + setPartitionKey("/" + value); + } else { + setPartitionKey(value); + } + }; + + const subPartitionKeysValueOnChange = (value: string, index: number): void => { + const updatedSubPartitionKeys: string[] = [...subPartitionKeys]; + if (!updatedSubPartitionKeys[index] && !value.startsWith("/")) { + updatedSubPartitionKeys[index] = "/" + value.trim(); + } else { + updatedSubPartitionKeys[index] = value.trim(); + } + setSubPartitionKeys(updatedSubPartitionKeys); + }; + + return ( + + + + + Partition key + + + + + + + ) => { + partitionKeyValueOnChange(event.target.value); + }} + /> + {subPartitionKeys.map((subPartitionKey: string, subPartitionKeyIndex: number) => { + return ( + +
    + 0 ? 1 : 0} + className="panelTextField" + autoComplete="off" + placeholder={getPartitionKeyPlaceHolder(subPartitionKeyIndex)} + aria-label={getPartitionKeyName()} + pattern={".*"} + title={""} + value={subPartitionKey} + onChange={(event: React.ChangeEvent) => { + subPartitionKeysValueOnChange(event.target.value, subPartitionKeyIndex); + }} + /> + { + const updatedSubPartitionKeys = subPartitionKeys.filter( + (_, subPartitionKeyIndexToRemove) => subPartitionKeyIndex !== subPartitionKeyIndexToRemove, + ); + setSubPartitionKeys(updatedSubPartitionKeys); + }} + /> +
    + ); + })} + + + {subPartitionKeys.length > 0 && ( + + This feature allows you to partition your + data with up to three levels of keys for better data distribution. Requires .NET V3, Java V4 SDK, or preview + JavaScript V3 SDK.{" "} + + Learn more + + + )} + +
    + ); +}; diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent.tsx b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent.tsx new file mode 100644 index 000000000..07669906f --- /dev/null +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent.tsx @@ -0,0 +1,71 @@ +import { Checkbox, Stack } from "@fluentui/react"; +import { ThroughputInput } from "Explorer/Controls/ThroughputInput/ThroughputInput"; +import { isFreeTierAccount } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; +import { useDatabases } from "Explorer/useDatabases"; +import React from "react"; +import { getCollectionName } from "Utils/APITypeUtils"; +import { isServerlessAccount } from "Utils/CapabilityUtils"; + +export interface ThroughputComponentProps { + enableDedicatedThroughput: boolean; + setEnabledDedicatedThroughput: React.Dispatch>; + isSelectedSourceContainerSharedThroughput: () => boolean; + showCollectionThroughputInput: () => boolean; + globalSecondaryIndexThroughputOnChange: (globalSecondaryIndexThroughputValue: number) => void; + isGlobalSecondaryIndexAutoscaleOnChange: (isGlobalSecondaryIndexAutoscaleValue: boolean) => void; + setIsThroughputCapExceeded: React.Dispatch>; + isCostAknowledgedOnChange: (isCostAknowledgedValue: boolean) => void; +} + +export const ThroughputComponent = (props: ThroughputComponentProps): JSX.Element => { + const { + enableDedicatedThroughput, + setEnabledDedicatedThroughput, + isSelectedSourceContainerSharedThroughput, + showCollectionThroughputInput, + globalSecondaryIndexThroughputOnChange, + isGlobalSecondaryIndexAutoscaleOnChange, + setIsThroughputCapExceeded, + isCostAknowledgedOnChange, + } = props; + + return ( + + {!isServerlessAccount() && isSelectedSourceContainerSharedThroughput() && ( + + setEnabledDedicatedThroughput(isChecked)} + /> + + )} + {showCollectionThroughputInput() && ( + { + globalSecondaryIndexThroughputOnChange(throughput); + }} + setIsAutoscale={(isAutoscale: boolean) => { + isGlobalSecondaryIndexAutoscaleOnChange(isAutoscale); + }} + setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => { + setIsThroughputCapExceeded(isThroughputCapExceeded); + }} + onCostAcknowledgeChange={(isAcknowledged: boolean) => { + isCostAknowledgedOnChange(isAcknowledged); + }} + /> + )} + + ); +}; diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent.tsx b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent.tsx new file mode 100644 index 000000000..ab46c8d41 --- /dev/null +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent.tsx @@ -0,0 +1,78 @@ +import { ActionButton, IconButton, Stack } from "@fluentui/react"; +import { UniqueKeysHeader } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; +import React from "react"; +import { userContext } from "UserContext"; + +export interface UniqueKeysComponentProps { + uniqueKeys: string[]; + setUniqueKeys: React.Dispatch>; +} + +export const UniqueKeysComponent = (props: UniqueKeysComponentProps): JSX.Element => { + const { uniqueKeys, setUniqueKeys } = props; + + const updateUniqueKeysOnChange = (value: string, uniqueKeyToReplaceIndex: number): void => { + const updatedUniqueKeys = uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number) => { + if (uniqueKeyToReplaceIndex === uniqueKeyIndex) { + return value; + } + return uniqueKey; + }); + setUniqueKeys(updatedUniqueKeys); + }; + + const deleteUniqueKeyOnClick = (uniqueKeyToDeleteIndex: number): void => { + const updatedUniqueKeys = uniqueKeys.filter((_, uniqueKeyIndex) => uniqueKeyToDeleteIndex !== uniqueKeyIndex); + setUniqueKeys(updatedUniqueKeys); + }; + + const addUniqueKeyOnClick = (): void => { + setUniqueKeys([...uniqueKeys, ""]); + }; + + return ( + + {UniqueKeysHeader()} + + {uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number): JSX.Element => { + return ( + + ) => { + updateUniqueKeysOnChange(event.target.value, uniqueKeyIndex); + }} + /> + + { + deleteUniqueKeyOnClick(uniqueKeyIndex); + }} + /> + + ); + })} + + { + addUniqueKeyOnClick(); + }} + > + Add unique key + + + ); +}; diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/VectorSearchComponent.tsx b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/VectorSearchComponent.tsx new file mode 100644 index 000000000..440d51ea4 --- /dev/null +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/VectorSearchComponent.tsx @@ -0,0 +1,58 @@ +import { Stack } from "@fluentui/react"; +import { VectorEmbedding, VectorIndex } from "Contracts/DataModels"; +import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent"; +import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent"; +import { + ContainerVectorPolicyTooltipContent, + scrollToSection, +} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; +import React from "react"; + +export interface VectorSearchComponentProps { + vectorEmbeddingPolicy: VectorEmbedding[]; + setVectorEmbeddingPolicy: React.Dispatch>; + vectorIndexingPolicy: VectorIndex[]; + setVectorIndexingPolicy: React.Dispatch>; + setVectorPolicyValidated: React.Dispatch>; +} + +export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.Element => { + const { + vectorEmbeddingPolicy, + setVectorEmbeddingPolicy, + vectorIndexingPolicy, + setVectorIndexingPolicy, + setVectorPolicyValidated, + } = props; + + return ( + + { + scrollToSection("collapsibleVectorPolicySectionContent"); + }} + tooltipContent={ContainerVectorPolicyTooltipContent()} + > + + + { + setVectorEmbeddingPolicy(vectorEmbeddingPolicy); + setVectorIndexingPolicy(vectorIndexingPolicy); + setVectorPolicyValidated(vectorPolicyValidated); + }} + /> + + + + + ); +}; diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap new file mode 100644 index 000000000..ddd1648d4 --- /dev/null +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap @@ -0,0 +1,190 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = ` +
    +
    + + + + *  + + + Source container id + + + + + + + *  + + + Global secondary index container id + + + + + + *  + + + Global secondary index definition + + + Learn more about defining global secondary indexes. + + } + directionalHint={4} + > + + + + + + + + + + +
    + + +`; diff --git a/src/Explorer/QueryCopilot/CopilotCarousel.tsx b/src/Explorer/QueryCopilot/CopilotCarousel.tsx index 4a73cacb8..a1273c910 100644 --- a/src/Explorer/QueryCopilot/CopilotCarousel.tsx +++ b/src/Explorer/QueryCopilot/CopilotCarousel.tsx @@ -18,7 +18,7 @@ import { createCollection } from "Common/dataAccess/createCollection"; import * as DataModels from "Contracts/DataModels"; import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator"; import Explorer from "Explorer/Explorer"; -import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel"; +import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; import { PromptCard } from "Explorer/QueryCopilot/PromptCard"; import { useDatabases } from "Explorer/useDatabases"; import { useCarousel } from "hooks/useCarousel"; diff --git a/src/Explorer/Sidebar.tsx b/src/Explorer/Sidebar.tsx index 01fd8a6d1..229518f9f 100644 --- a/src/Explorer/Sidebar.tsx +++ b/src/Explorer/Sidebar.tsx @@ -13,9 +13,15 @@ import { SplitButton, } from "@fluentui/react-components"; import { Add16Regular, ArrowSync12Regular, ChevronLeft12Regular, ChevronRight12Regular } from "@fluentui/react-icons"; +import { GlobalSecondaryIndexLabels } from "Common/Constants"; +import { isGlobalSecondaryIndexEnabled } from "Common/DatabaseAccountUtility"; import { configContext, Platform } from "ConfigContext"; import Explorer from "Explorer/Explorer"; import { AddDatabasePanel } from "Explorer/Panes/AddDatabasePanel/AddDatabasePanel"; +import { + AddGlobalSecondaryIndexPanel, + AddGlobalSecondaryIndexPanelProps, +} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel"; import { Tabs } from "Explorer/Tabs/Tabs"; import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil"; import { ResourceTree } from "Explorer/Tree/ResourceTree"; @@ -162,6 +168,25 @@ const GlobalCommands: React.FC = ({ explorer }) => { }); } + if (isGlobalSecondaryIndexEnabled()) { + const addMaterializedViewPanelProps: AddGlobalSecondaryIndexPanelProps = { + explorer, + }; + + actions.push({ + id: "new_materialized_view", + label: GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex, + icon: , + onClick: () => + useSidePanel + .getState() + .openSidePanel( + GlobalSecondaryIndexLabels.NewGlobalSecondaryIndex, + , + ), + }); + } + return actions; }, [explorer]); diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index c692d0747..bc3af39f1 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -58,6 +58,8 @@ export default class Collection implements ViewModels.Collection { public uniqueKeyPolicy: DataModels.UniqueKeyPolicy; public usageSizeInKB: ko.Observable; public computedProperties: ko.Observable; + public materializedViews: ko.Observable; + public materializedViewDefinition: ko.Observable; public offer: ko.Observable; public conflictResolutionPolicy: ko.Observable; @@ -124,6 +126,8 @@ export default class Collection implements ViewModels.Collection { this.requestSchema = data.requestSchema; this.geospatialConfig = ko.observable(data.geospatialConfig); this.computedProperties = ko.observable(data.computedProperties); + this.materializedViews = ko.observable(data.materializedViews); + this.materializedViewDefinition = ko.observable(data.materializedViewDefinition); this.partitionKeyPropertyHeaders = this.partitionKey?.paths; this.partitionKeyProperties = this.partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader, i) => { diff --git a/src/Explorer/Tree/Database.tsx b/src/Explorer/Tree/Database.tsx index 0ed11cb5d..5ad2769d8 100644 --- a/src/Explorer/Tree/Database.tsx +++ b/src/Explorer/Tree/Database.tsx @@ -19,7 +19,7 @@ import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import { useSidePanel } from "../../hooks/useSidePanel"; import { useTabs } from "../../hooks/useTabs"; import Explorer from "../Explorer"; -import { AddCollectionPanel } from "../Panes/AddCollectionPanel"; +import { AddCollectionPanel } from "../Panes/AddCollectionPanel/AddCollectionPanel"; import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2"; import { useDatabases } from "../useDatabases"; import { useSelectedNode } from "../useSelectedNode"; diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 10b8316c0..60e67ecda 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -1,6 +1,7 @@ import { Tree, TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components"; import { Home16Regular } from "@fluentui/react-icons"; import { AuthType } from "AuthType"; +import { Collection } from "Contracts/ViewModels"; import { useTreeStyles } from "Explorer/Controls/TreeComponent/Styles"; import { TreeNode, TreeNodeComponent } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { @@ -60,7 +61,7 @@ export const ResourceTree: React.FC = ({ explorer }: Resource const databaseTreeNodes = useMemo(() => { return userContext.authType === AuthType.ResourceToken - ? createResourceTokenTreeNodes(resourceTokenCollection) + ? createResourceTokenTreeNodes(resourceTokenCollection as Collection) : createDatabaseTreeNodes(explorer, isNotebookEnabled, databases, refreshActiveTab); }, [resourceTokenCollection, databases, isNotebookEnabled, refreshActiveTab]); diff --git a/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap b/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap index 9814a72fb..3515c1257 100644 --- a/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap +++ b/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap @@ -30,7 +30,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -72,7 +72,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -145,7 +145,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -264,7 +264,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -369,7 +369,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -442,7 +442,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -546,7 +546,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -696,7 +696,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -760,7 +760,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -787,7 +787,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -841,7 +841,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -895,7 +895,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -953,7 +953,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "onClick": [Function], }, ], - "iconSrc": , "isExpanded": true, @@ -974,7 +974,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "onClick": [Function], }, ], - "iconSrc": , "isExpanded": true, @@ -1010,7 +1010,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "onClick": [Function], }, ], - "iconSrc": , "isExpanded": true, @@ -1046,7 +1046,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "onClick": [Function], }, ], - "iconSrc": , "isExpanded": true, @@ -1208,7 +1208,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -1311,7 +1311,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -1445,7 +1445,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -1625,7 +1625,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the SQ "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -1799,7 +1799,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -1897,7 +1897,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -2031,7 +2031,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -2211,7 +2211,7 @@ exports[`createDatabaseTreeNodes using NoSQL API on Hosted Platform creates expe "styleClass": "deleteCollectionMenuItem", }, ], - "iconSrc": , "isExpanded": true, @@ -2266,7 +2266,7 @@ exports[`createResourceTokenTreeNodes creates the expected tree nodes 1`] = ` }, ], "className": "collectionNode", - "iconSrc": , "isExpanded": true, diff --git a/src/Explorer/Tree/treeNodeUtil.test.ts b/src/Explorer/Tree/treeNodeUtil.test.ts index f9298a428..8172ade3a 100644 --- a/src/Explorer/Tree/treeNodeUtil.test.ts +++ b/src/Explorer/Tree/treeNodeUtil.test.ts @@ -82,6 +82,7 @@ jest.mock("Explorer/Tree/Trigger", () => { jest.mock("Common/DatabaseAccountUtility", () => { return { isPublicInternetAccessAllowed: () => true, + isGlobalSecondaryIndexEnabled: () => false, }; }); @@ -134,6 +135,15 @@ const baseCollection = { kind: "hash", version: 2, }, + materializedViews: ko.observable([ + { id: "view1", _rid: "rid1" }, + { id: "view2", _rid: "rid2" }, + ]), + materializedViewDefinition: ko.observable({ + definition: "SELECT * FROM c WHERE c.id = 1", + sourceCollectionId: "source1", + sourceCollectionRid: "rid123", + }), storedProcedures: ko.observableArray([]), userDefinedFunctions: ko.observableArray([]), triggers: ko.observableArray([]), diff --git a/src/Explorer/Tree/treeNodeUtil.tsx b/src/Explorer/Tree/treeNodeUtil.tsx index 60fe9079b..a9bf32247 100644 --- a/src/Explorer/Tree/treeNodeUtil.tsx +++ b/src/Explorer/Tree/treeNodeUtil.tsx @@ -1,4 +1,4 @@ -import { DatabaseRegular, DocumentMultipleRegular, SettingsRegular } from "@fluentui/react-icons"; +import { DatabaseRegular, DocumentMultipleRegular, EyeRegular, SettingsRegular } from "@fluentui/react-icons"; import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity"; import TabsBase from "Explorer/Tabs/TabsBase"; @@ -29,6 +29,7 @@ export const shouldShowScriptNodes = (): boolean => { const TreeDatabaseIcon = ; const TreeSettingsIcon = ; const TreeCollectionIcon = ; +const GlobalSecondaryIndexCollectionIcon = ; //check icon export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: ViewModels.CollectionBase): TreeNode[] => { const updatedSampleTree: TreeNode = { @@ -80,7 +81,7 @@ export const createSampleDataTreeNodes = (sampleDataResourceTokenCollection: Vie return [updatedSampleTree]; }; -export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBase): TreeNode[] => { +export const createResourceTokenTreeNodes = (collection: ViewModels.Collection): TreeNode[] => { if (!collection) { return [ { @@ -110,7 +111,7 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa isExpanded: true, children, className: "collectionNode", - iconSrc: TreeCollectionIcon, + iconSrc: collection.materializedViewDefinition() ? GlobalSecondaryIndexCollectionIcon : TreeCollectionIcon, onClick: () => { // Rewritten version of expandCollapseCollection useSelectedNode.getState().setSelectedNode(collection); @@ -228,7 +229,7 @@ export const buildCollectionNode = ( children: children, className: "collectionNode", contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection), - iconSrc: TreeCollectionIcon, + iconSrc: collection.materializedViewDefinition() ? GlobalSecondaryIndexCollectionIcon : TreeCollectionIcon, onClick: () => { useSelectedNode.getState().setSelectedNode(collection); collection.openTab(); diff --git a/src/Shared/Telemetry/TelemetryConstants.ts b/src/Shared/Telemetry/TelemetryConstants.ts index 1fae132ad..e0f1691be 100644 --- a/src/Shared/Telemetry/TelemetryConstants.ts +++ b/src/Shared/Telemetry/TelemetryConstants.ts @@ -2,6 +2,7 @@ export enum Action { CollapseTreeNode, CreateCollection, + CreateGlobalSecondaryIndex, CreateDocument, CreateStoredProcedure, CreateTrigger, @@ -119,6 +120,7 @@ export enum Action { NotebooksGalleryPublishedCount, SelfServe, ExpandAddCollectionPaneAdvancedSection, + ExpandAddGlobalSecondaryIndexPaneAdvancedSection, SchemaAnalyzerClickAnalyze, SelfServeComponent, LaunchQuickstart, From afdbefe36c3b833b428331a0a8e37097a4894233 Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Tue, 15 Apr 2025 17:48:44 +0200 Subject: [PATCH 08/87] Implement refreshResourceTree message and hide refresh button above resource tree for Fabric native (#2102) --- src/Contracts/FabricMessagesContract.ts | 7 +++++++ src/Explorer/Sidebar.tsx | 22 ++++++++++++---------- src/hooks/useKnockoutExplorer.ts | 4 ++++ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/Contracts/FabricMessagesContract.ts b/src/Contracts/FabricMessagesContract.ts index 2cc99c578..8cc198a11 100644 --- a/src/Contracts/FabricMessagesContract.ts +++ b/src/Contracts/FabricMessagesContract.ts @@ -81,6 +81,13 @@ export type FabricMessageV3 = error: string | undefined; data: { accessToken: string }; }; + } + | { + type: "refreshResourceTree"; + message: { + id: string; + error: string | undefined; + }; }; export enum CosmosDbArtifactType { diff --git a/src/Explorer/Sidebar.tsx b/src/Explorer/Sidebar.tsx index 229518f9f..f3db58d8a 100644 --- a/src/Explorer/Sidebar.tsx +++ b/src/Explorer/Sidebar.tsx @@ -340,16 +340,18 @@ export const SidebarContainer: React.FC = ({ explorer }) => { <>
    - + {!isFabricNative() && ( + + )} + + + + + ); +}; diff --git a/src/Explorer/SplashScreen/SampleUtil.ts b/src/Explorer/SplashScreen/SampleUtil.ts new file mode 100644 index 000000000..837227c8f --- /dev/null +++ b/src/Explorer/SplashScreen/SampleUtil.ts @@ -0,0 +1,56 @@ +import { createCollection } from "Common/dataAccess/createCollection"; +import Explorer from "Explorer/Explorer"; +import { useDatabases } from "Explorer/useDatabases"; +import * as DataModels from "../../Contracts/DataModels"; +import * as ViewModels from "../../Contracts/ViewModels"; + +/** + * Public for unit tests + * @param databaseName + * @param containerName + * @param containerDatabases + */ +const hasContainer = ( + databaseName: string, + containerName: string, + containerDatabases: ViewModels.Database[], +): boolean => { + const filteredDatabases = containerDatabases.filter((database) => database.id() === databaseName); + return ( + filteredDatabases.length > 0 && + filteredDatabases[0].collections().filter((collection) => collection.id() === containerName).length > 0 + ); +}; + +export const checkContainerExists = (databaseName: string, containerName: string) => + hasContainer(databaseName, containerName, useDatabases.getState().databases); + +export const createContainer = async ( + databaseName: string, + containerName: string, + explorer: Explorer, +): Promise => { + const createRequest: DataModels.CreateCollectionParams = { + createNewDatabase: false, + collectionId: containerName, + databaseId: databaseName, + databaseLevelThroughput: false, + }; + await createCollection(createRequest); + await explorer.refreshAllDatabases(); + const database = useDatabases.getState().findDatabaseWithId(databaseName); + if (!database) { + return undefined; + } + await database.loadCollections(); + const newCollection = database.findCollectionWithId(containerName); + return newCollection; +}; + +export const importData = async (collection: ViewModels.Collection): Promise => { + // TODO: keep same chunk as ContainerSampleGenerator + const dataFileContent = await import( + /* webpackChunkName: "queryCopilotSampleData" */ "../../../sampleData/queryCopilotSampleData.json" + ); + await collection.bulkInsertDocuments(dataFileContent.data); +}; diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 096b9e087..d44c99ac3 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -1,4 +1,10 @@ -import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos"; +import { + JSONObject, + Resource, + StoredProcedureDefinition, + TriggerDefinition, + UserDefinedFunctionDefinition, +} from "@azure/cosmos"; import { useNotebook } from "Explorer/Notebook/useNotebook"; import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; @@ -1086,6 +1092,56 @@ export default class Collection implements ViewModels.Collection { }); } + 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[], + }; + + const chunkSize = 100; // 100 is the max # of bulk operations the SDK currently accepts + const chunkedContent = Array.from({ length: Math.ceil(documents.length / chunkSize) }, (_, index) => + documents.slice(index * chunkSize, index * chunkSize + chunkSize), + ); + for (const chunk of chunkedContent) { + let retryAttempts = 0; + let chunkComplete = false; + let documentsToAttempt = chunk; + while (retryAttempts < 10 && !chunkComplete) { + const responses = await bulkCreateDocument(this, documentsToAttempt); + const attemptedDocuments = [...documentsToAttempt]; + documentsToAttempt = []; + responses.forEach((response, index) => { + if (response.statusCode === 201) { + stats.numSucceeded++; + } else if (response.statusCode === 429) { + documentsToAttempt.push(attemptedDocuments[index]); + } else { + stats.numFailed++; + stats.errors.push(JSON.stringify(response.resourceBody)); + } + }); + if (documentsToAttempt.length === 0) { + chunkComplete = true; + break; + } + logConsoleInfo( + `${documentsToAttempt.length} document creations were throttled. Waiting ${retryAttempts} seconds and retrying throttled documents`, + ); + retryAttempts++; + await sleep(retryAttempts); + } + } + + return stats; + } + private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise { const record: UploadDetailsRecord = { fileName: fileName, @@ -1098,38 +1154,11 @@ export default class Collection implements ViewModels.Collection { try { const parsedContent = JSON.parse(documentContent); if (Array.isArray(parsedContent)) { - const chunkSize = 100; // 100 is the max # of bulk operations the SDK currently accepts - const chunkedContent = Array.from({ length: Math.ceil(parsedContent.length / chunkSize) }, (_, index) => - parsedContent.slice(index * chunkSize, index * chunkSize + chunkSize), - ); - for (const chunk of chunkedContent) { - let retryAttempts = 0; - let chunkComplete = false; - let documentsToAttempt = chunk; - while (retryAttempts < 10 && !chunkComplete) { - const responses = await bulkCreateDocument(this, documentsToAttempt); - const attemptedDocuments = [...documentsToAttempt]; - documentsToAttempt = []; - responses.forEach((response, index) => { - if (response.statusCode === 201) { - record.numSucceeded++; - } else if (response.statusCode === 429) { - documentsToAttempt.push(attemptedDocuments[index]); - } else { - record.numFailed++; - } - }); - if (documentsToAttempt.length === 0) { - chunkComplete = true; - break; - } - logConsoleInfo( - `${documentsToAttempt.length} document creations were throttled. Waiting ${retryAttempts} seconds and retrying throttled documents`, - ); - retryAttempts++; - await sleep(retryAttempts); - } - } + const { numSucceeded, numFailed, numThrottled, errors } = await this.bulkInsertDocuments(parsedContent); + record.numSucceeded = numSucceeded; + record.numFailed = numFailed; + record.numThrottled = numThrottled; + record.errors = errors; } else { await createDocument(this, parsedContent); record.numSucceeded++; From af4e1d10b44538dac917698fbfb77884cf530ef1 Mon Sep 17 00:00:00 2001 From: asier-isayas Date: Fri, 18 Apr 2025 14:39:31 -0400 Subject: [PATCH 15/87] GSI: Remove Unique Key Policy and Manual Throughput (#2114) * Remove Unique Key Policy and Manual Throughput * fix tests * remove manual throughput option from scale & settings * fix test * cleanup --------- Co-authored-by: Asier Isayas --- .../Controls/Settings/SettingsComponent.tsx | 1 + .../ScaleComponent.test.tsx | 1 + .../SettingsSubComponents/ScaleComponent.tsx | 2 + ...roughputInputAutoPilotV3Component.test.tsx | 1 + .../ThroughputInputAutoPilotV3Component.tsx | 35 +++++---- .../SettingsComponent.test.tsx.snap | 1 + .../ThroughputInput/ThroughputInput.tsx | 70 +++++++++-------- .../AddGlobalSecondaryIndexPanel.tsx | 45 +++-------- .../Components/ThroughputComponent.tsx | 7 +- .../Components/UniqueKeysComponent.tsx | 78 ------------------- ...AddGlobalSecondaryIndexPanel.test.tsx.snap | 5 -- 11 files changed, 74 insertions(+), 172 deletions(-) delete mode 100644 src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent.tsx diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 2613fe65b..684858c56 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -1149,6 +1149,7 @@ export class SettingsComponent extends React.Component { collection: collection, database: undefined, isFixedContainer: false, + isGlobalSecondaryIndex: false, onThroughputChange: () => { return; }, diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx index 251a3b841..1928a68e6 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ScaleComponent.tsx @@ -22,6 +22,7 @@ export interface ScaleComponentProps { collection: ViewModels.Collection; database: ViewModels.Database; isFixedContainer: boolean; + isGlobalSecondaryIndex: boolean; onThroughputChange: (newThroughput: number) => void; throughput: number; throughputBaseline: number; @@ -143,6 +144,7 @@ export class ScaleComponent extends React.Component { throughputError={this.props.throughputError} instantMaximumThroughput={this.offer?.instantMaximumThroughput} softAllowedMaximumThroughput={this.offer?.softAllowedMaximumThroughput} + isGlobalSecondaryIndex={this.props.isGlobalSecondaryIndex} /> ); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx index ff7b715b3..a02d907b3 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.test.tsx @@ -44,6 +44,7 @@ describe("ThroughputInputAutoPilotV3Component", () => { }, instantMaximumThroughput: 5000, softAllowedMaximumThroughput: 1000000, + isGlobalSecondaryIndex: false, }; it("throughput input visible", () => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx index 391789938..2ae5dcfc9 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputInputAutoPilotV3Component.tsx @@ -80,6 +80,7 @@ export interface ThroughputInputAutoPilotV3Props { throughputError?: string; instantMaximumThroughput: number; softAllowedMaximumThroughput: number; + isGlobalSecondaryIndex: boolean; } interface ThroughputInputAutoPilotV3State { @@ -375,22 +376,26 @@ export class ThroughputInputAutoPilotV3Component extends React.Component< toolTipElement={getToolTipContainer(this.props.infoBubbleText)} /> - {this.overrideWithProvisionedThroughputSettings() && ( - - {manualToAutoscaleDisclaimerElement} - + {!this.props.isGlobalSecondaryIndex && ( + <> + {this.overrideWithProvisionedThroughputSettings() && ( + + {manualToAutoscaleDisclaimerElement} + + )} + + )} - ); }; diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index ac2aa4fa3..7a54134db 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -79,6 +79,7 @@ exports[`SettingsComponent renders 1`] = ` } isAutoPilotSelected={false} isFixedContainer={false} + isGlobalSecondaryIndex={true} onAutoPilotSelected={[Function]} onMaxAutoPilotThroughputChange={[Function]} onScaleDiscardableChange={[Function]} diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx index 9fb5b28f9..af0219be1 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx @@ -18,6 +18,7 @@ export interface ThroughputInputProps { isFreeTier: boolean; showFreeTierExceedThroughputTooltip: boolean; isQuickstart?: boolean; + isGlobalSecondaryIndex?: boolean; setThroughputValue: (throughput: number) => void; setIsAutoscale: (isAutoscale: boolean) => void; setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void; @@ -30,6 +31,7 @@ export const ThroughputInput: FunctionComponent = ({ isFreeTier, showFreeTierExceedThroughputTooltip, isQuickstart, + isGlobalSecondaryIndex, setThroughputValue, setIsAutoscale, setIsThroughputCapExceeded, @@ -193,41 +195,41 @@ export const ThroughputInput: FunctionComponent = ({ {PricingUtils.getRuToolTipText()} + {!isGlobalSecondaryIndex && ( + +
    + handleOnChangeMode(e, "Autoscale")} + /> + - -
    - handleOnChangeMode(e, "Autoscale")} - /> - - - handleOnChangeMode(e, "Manual")} - /> - -
    -
    - + handleOnChangeMode(e, "Manual")} + /> + +
    +
    + )} {isAutoscaleSelected && ( diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel.tsx b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel.tsx index 313c72b4c..ba49299fb 100644 --- a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel.tsx +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel.tsx @@ -22,7 +22,6 @@ import { FullTextPolicyDefault, getPartitionKey, isSynapseLinkEnabled, - parseUniqueKeys, scrollToSection, shouldShowAnalyticalStoreOptions, } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; @@ -35,7 +34,6 @@ import { AnalyticalStoreComponent } from "Explorer/Panes/AddGlobalSecondaryIndex import { FullTextSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/FullTextSearchComponent"; import { PartitionKeyComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/PartitionKeyComponent"; import { ThroughputComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent"; -import { UniqueKeysComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent"; import { VectorSearchComponent } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/VectorSearchComponent"; import { PanelFooterComponent } from "Explorer/Panes/PanelFooterComponent"; import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent"; @@ -66,14 +64,13 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel const [useHashV1, setUseHashV1] = useState(); const [enableDedicatedThroughput, setEnabledDedicatedThroughput] = useState(); const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState(); - const [uniqueKeys, setUniqueKeys] = useState([]); const [enableAnalyticalStore, setEnableAnalyticalStore] = useState(); - const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState(); - const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState(); - const [vectorPolicyValidated, setVectorPolicyValidated] = useState(); + const [vectorEmbeddingPolicy, setVectorEmbeddingPolicy] = useState([]); + const [vectorIndexingPolicy, setVectorIndexingPolicy] = useState([]); + const [vectorPolicyValidated, setVectorPolicyValidated] = useState(true); const [fullTextPolicy, setFullTextPolicy] = useState(FullTextPolicyDefault); - const [fullTextIndexes, setFullTextIndexes] = useState(); - const [fullTextPolicyValidated, setFullTextPolicyValidated] = useState(); + const [fullTextIndexes, setFullTextIndexes] = useState([]); + const [fullTextPolicyValidated, setFullTextPolicyValidated] = useState(true); const [errorMessage, setErrorMessage] = useState(); const [showErrorDetails, setShowErrorDetails] = useState(); const [isExecuting, setIsExecuting] = useState(); @@ -109,17 +106,12 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel }, [errorMessage]); let globalSecondaryIndexThroughput: number; - let isGlobalSecondaryIndexAutoscale: boolean; let isCostAcknowledged: boolean; const globalSecondaryIndexThroughputOnChange = (globalSecondaryIndexThroughputValue: number): void => { globalSecondaryIndexThroughput = globalSecondaryIndexThroughputValue; }; - const isGlobalSecondaryIndexAutoscaleOnChange = (isGlobalSecondaryIndexAutoscaleValue: boolean): void => { - isGlobalSecondaryIndexAutoscale = isGlobalSecondaryIndexAutoscaleValue; - }; - const isCostAknowledgedOnChange = (isCostAcknowledgedValue: boolean): void => { isCostAcknowledged = isCostAcknowledgedValue; }; @@ -178,9 +170,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel } if (globalSecondaryIndexThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) { - const errorMessage = isGlobalSecondaryIndexAutoscale - ? "Please acknowledge the estimated monthly spend." - : "Please acknowledge the estimated daily spend."; + const errorMessage: string = "Please acknowledge the estimated monthly spend."; setErrorMessage(errorMessage); return false; } @@ -221,7 +211,6 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel const partitionKeyTrimmed: string = partitionKey.trim(); - const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = parseUniqueKeys(uniqueKeys); const partitionKeyVersion = useHashV1 ? undefined : 2; const partitionKeyPaths: DataModels.PartitionKey = partitionKeyTrimmed ? { @@ -256,9 +245,8 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel collection: { id: globalSecondaryIdTrimmed, throughput: globalSecondaryIndexThroughput, - isAutoscale: isGlobalSecondaryIndexAutoscale, + isAutoscale: true, partitionKeyPaths, - uniqueKeyPolicy, collectionWithDedicatedThroughput: enableDedicatedThroughput, }, subscriptionQuotaId: userContext.quotaId, @@ -268,28 +256,17 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel const startKey: number = TelemetryProcessor.traceStart(Action.CreateCollection, telemetryData); const databaseLevelThroughput: boolean = isSelectedSourceContainerSharedThroughput() && !enableDedicatedThroughput; - let offerThroughput: number; - let autoPilotMaxThroughput: number; - - if (!databaseLevelThroughput) { - if (isGlobalSecondaryIndexAutoscale) { - autoPilotMaxThroughput = globalSecondaryIndexThroughput; - } else { - offerThroughput = globalSecondaryIndexThroughput; - } - } - const createGlobalSecondaryIndexParams: DataModels.CreateMaterializedViewsParams = { materializedViewId: globalSecondaryIdTrimmed, materializedViewDefinition: globalSecondaryIndexDefinition, databaseId: selectedSourceContainer.databaseId, databaseLevelThroughput: databaseLevelThroughput, - offerThroughput: offerThroughput, - autoPilotMaxThroughput: autoPilotMaxThroughput, + ...(!databaseLevelThroughput && { + autoPilotMaxThroughput: globalSecondaryIndexThroughput, + }), analyticalStorageTtl: getAnalyticalStorageTtl(), indexingPolicy: indexingPolicy, partitionKey: partitionKeyPaths, - uniqueKeyPolicy: uniqueKeyPolicy, vectorEmbeddingPolicy: vectorEmbeddingPolicyFinal, fullTextPolicy: fullTextPolicy, }; @@ -395,12 +372,10 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel isSelectedSourceContainerSharedThroughput, showCollectionThroughputInput, globalSecondaryIndexThroughputOnChange, - isGlobalSecondaryIndexAutoscaleOnChange, setIsThroughputCapExceeded, isCostAknowledgedOnChange, }} /> - {shouldShowAnalyticalStoreOptions() && ( )} diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent.tsx b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent.tsx index 07669906f..0999acc5a 100644 --- a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent.tsx +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/ThroughputComponent.tsx @@ -12,7 +12,6 @@ export interface ThroughputComponentProps { isSelectedSourceContainerSharedThroughput: () => boolean; showCollectionThroughputInput: () => boolean; globalSecondaryIndexThroughputOnChange: (globalSecondaryIndexThroughputValue: number) => void; - isGlobalSecondaryIndexAutoscaleOnChange: (isGlobalSecondaryIndexAutoscaleValue: boolean) => void; setIsThroughputCapExceeded: React.Dispatch>; isCostAknowledgedOnChange: (isCostAknowledgedValue: boolean) => void; } @@ -24,7 +23,6 @@ export const ThroughputComponent = (props: ThroughputComponentProps): JSX.Elemen isSelectedSourceContainerSharedThroughput, showCollectionThroughputInput, globalSecondaryIndexThroughputOnChange, - isGlobalSecondaryIndexAutoscaleOnChange, setIsThroughputCapExceeded, isCostAknowledgedOnChange, } = props; @@ -52,12 +50,11 @@ export const ThroughputComponent = (props: ThroughputComponentProps): JSX.Elemen isSharded={false} isFreeTier={isFreeTierAccount()} isQuickstart={false} + isGlobalSecondaryIndex={true} setThroughputValue={(throughput: number) => { globalSecondaryIndexThroughputOnChange(throughput); }} - setIsAutoscale={(isAutoscale: boolean) => { - isGlobalSecondaryIndexAutoscaleOnChange(isAutoscale); - }} + setIsAutoscale={() => {}} setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => { setIsThroughputCapExceeded(isThroughputCapExceeded); }} diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent.tsx b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent.tsx deleted file mode 100644 index ab46c8d41..000000000 --- a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/Components/UniqueKeysComponent.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { ActionButton, IconButton, Stack } from "@fluentui/react"; -import { UniqueKeysHeader } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; -import React from "react"; -import { userContext } from "UserContext"; - -export interface UniqueKeysComponentProps { - uniqueKeys: string[]; - setUniqueKeys: React.Dispatch>; -} - -export const UniqueKeysComponent = (props: UniqueKeysComponentProps): JSX.Element => { - const { uniqueKeys, setUniqueKeys } = props; - - const updateUniqueKeysOnChange = (value: string, uniqueKeyToReplaceIndex: number): void => { - const updatedUniqueKeys = uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number) => { - if (uniqueKeyToReplaceIndex === uniqueKeyIndex) { - return value; - } - return uniqueKey; - }); - setUniqueKeys(updatedUniqueKeys); - }; - - const deleteUniqueKeyOnClick = (uniqueKeyToDeleteIndex: number): void => { - const updatedUniqueKeys = uniqueKeys.filter((_, uniqueKeyIndex) => uniqueKeyToDeleteIndex !== uniqueKeyIndex); - setUniqueKeys(updatedUniqueKeys); - }; - - const addUniqueKeyOnClick = (): void => { - setUniqueKeys([...uniqueKeys, ""]); - }; - - return ( - - {UniqueKeysHeader()} - - {uniqueKeys.map((uniqueKey: string, uniqueKeyIndex: number): JSX.Element => { - return ( - - ) => { - updateUniqueKeysOnChange(event.target.value, uniqueKeyIndex); - }} - /> - - { - deleteUniqueKeyOnClick(uniqueKeyIndex); - }} - /> - - ); - })} - - { - addUniqueKeyOnClick(); - }} - > - Add unique key - - - ); -}; diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap index 143c1f804..cef30c445 100644 --- a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap +++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap @@ -137,16 +137,11 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = ` - Date: Mon, 28 Apr 2025 07:13:24 +0530 Subject: [PATCH 16/87] Mongo RU Schema Analyzer Deprecation (#2117) * remove menu item * remove unused import --- src/Explorer/Tree/ResourceTreeAdapter.tsx | 15 -------------- .../__snapshots__/treeNodeUtil.test.ts.snap | 20 ------------------- src/Explorer/Tree/treeNodeUtil.tsx | 18 ----------------- 3 files changed, 53 deletions(-) diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 76d3e9308..819c73d6c 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -241,21 +241,6 @@ export class ResourceTreeAdapter implements ReactAdapter { contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection), }); - if ( - useNotebook.getState().isNotebookEnabled && - userContext.apiType === "Mongo" && - isPublicInternetAccessAllowed() - ) { - children.push({ - label: "Schema (Preview)", - onClick: collection.onSchemaAnalyzerClick.bind(collection), - isSelected: () => - useSelectedNode - .getState() - .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]), - }); - } - if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) { children.push({ label: database.isDatabaseShared() || isServerlessAccount() ? "Settings" : "Scale & Settings", diff --git a/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap b/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap index 8b7336600..181f38f2f 100644 --- a/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap +++ b/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap @@ -338,11 +338,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo "label": "Documents", "onClick": [Function], }, - { - "isSelected": [Function], - "label": "Schema (Preview)", - "onClick": [Function], - }, { "id": "", "isSelected": [Function], @@ -406,11 +401,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo "label": "Documents", "onClick": [Function], }, - { - "isSelected": [Function], - "label": "Schema (Preview)", - "onClick": [Function], - }, { "id": "", "isSelected": [Function], @@ -515,11 +505,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo "label": "Documents", "onClick": [Function], }, - { - "isSelected": [Function], - "label": "Schema (Preview)", - "onClick": [Function], - }, { "id": "sampleSettings", "isSelected": [Function], @@ -610,11 +595,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo "label": "Documents", "onClick": [Function], }, - { - "isSelected": [Function], - "label": "Schema (Preview)", - "onClick": [Function], - }, { "id": "", "isSelected": [Function], diff --git a/src/Explorer/Tree/treeNodeUtil.tsx b/src/Explorer/Tree/treeNodeUtil.tsx index f7f7a764c..838d5c1f0 100644 --- a/src/Explorer/Tree/treeNodeUtil.tsx +++ b/src/Explorer/Tree/treeNodeUtil.tsx @@ -11,7 +11,6 @@ import { getItemName } from "Utils/APITypeUtils"; import { isServerlessAccount } from "Utils/CapabilityUtils"; import { useTabs } from "hooks/useTabs"; import React from "react"; -import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import { Platform, configContext } from "../../ConfigContext"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; @@ -19,7 +18,6 @@ import { userContext } from "../../UserContext"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; -import { useNotebook } from "../Notebook/useNotebook"; import { useSelectedNode } from "../useSelectedNode"; export const shouldShowScriptNodes = (): boolean => { @@ -294,22 +292,6 @@ const buildCollectionNodeChildren = ( contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection), }); - if ( - isNotebookEnabled && - userContext.apiType === "Mongo" && - isPublicInternetAccessAllowed() && - useNotebook.getState().isPhoenixFeatures - ) { - children.push({ - label: "Schema (Preview)", - onClick: collection.onSchemaAnalyzerClick.bind(collection), - isSelected: () => - useSelectedNode - .getState() - .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]), - }); - } - if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) { let id = ""; if (collection.isSampleCollection) { From 6db2536a61478259fde0ffda4065a3ea2f96c4f1 Mon Sep 17 00:00:00 2001 From: Nishtha Ahuja <45535788+nishthaAhujaa@users.noreply.github.com> Date: Mon, 28 Apr 2025 21:11:27 +0530 Subject: [PATCH 17/87] fixed quickstart tab in emulator (#2115) Co-authored-by: nishthaAhujaa --- images/dotnet.png | Bin 7074 -> 2140 bytes images/golang.svg | 8 ++ images/springboot.svg | 10 ++ src/quickstart.html | 253 ++++++++++++++---------------------------- 4 files changed, 101 insertions(+), 170 deletions(-) create mode 100644 images/golang.svg create mode 100644 images/springboot.svg diff --git a/images/dotnet.png b/images/dotnet.png index c8972efdc3611383ed914f525c120e5e64d858e4..fb00ecf91e4b78804c636194bb323bf3710fa1c6 100644 GIT binary patch literal 2140 zcmeHIX;4#F6iz}QBp3(*v25~!F*qzP0ojoU1Wja7KpD}12oa>Xrfece2to*M4{<@1 zP%%ms&>BGn2e5_86S0m&0z?*tFd77$B7y>vzNa&tPJedV>7UNLH{U(qJ@=gNJNL&G zZwy{XCYg~i7z~-iW`$xfSQ!0vwGifWRzWuc0UHB1`G?p&M?Q^4^TQdnylqlFJQMHV zlMy}8cyoMm;xpH^t5o#5@y2-k+Mdkle$k#mS^4OrhKW<@s@vsbjW^%%!+QI>rn#<) z{_bgV=B_HFEH)`LIBec*h>(fF5SlnFpG|4X(PvmP2D6~~`@Sai?5+nm4-!M>!#e`& z78+VFVXe(SMlq!^eg7xWd6boUvM4P&8=5T0wgi! zp<$6Qus?iG)+d;(0n8C!5))oCLL24Oym*Gn(Wz_qt3FCV776EaQ0@8?Z;*X?j^|Sm zn!Z?CE$ZK^Bel^@7%D^$=pOWDB0`L52FJhnR;@@*4VX&YV4t-$6D&!6S(50;t4kpO zSu@uc7lG;JP{4;;gTKszM|=8RA1W@lCalj952n{cQ<2H{5Ho%XwCWD_{Z4eMJK>$z zk!wqDgEouoAc>PS^6@?wtqyq}w+)qQ-k}&oy@>2Jq0>$(&(2yz!kQ$B-aeP1JVAwi zLdDN$6HyHca*;h+GLwCO>;wmB&|!(*}Njk+((AJ3{PGOdx_ow6Z(=O$zl0DfhA9qWN%*0384ZX-%PdlCVsU;^WuLWS6 zFXEf8|L9P?|BxkhKeqYq++65zuqrIXB_=k7X0>One(W+8@jAr|KOqE zA{8rpb%xTK*LyH?wH7r3VwmvsXBR83fP-{+TLwEzY04n+9E2=$_+^Svj1xydpdM_| zOAWL@lsIQ|g;s%d*cN6$i7RV!TL3t`Nippk%z?;2>&ui}v9{Qry++zPRCad)IAFrl zVrN|@85!_xg;Kwn%BW~--xx!>Wk=-d&Bet~zEI_-Jkz#l_a=BNo+EN{T-sGgLK>2R zTl1?679M`Te?VAqflxs-Xlp(?+z^mfcBjCASoPc0Eo+$1Ukv);wnZsCeH&`cr6V+- zlv98Q2P&n*!Bn0NQC5WS;Rxq;oYdzbZwp4})3$-jUb&QY)+bmNVpr+``XIZdFn@{R z-y&lERCUNMmld3Uk>W<<`>Kw>#6lx$oBxuCIFtk;kF{VWJb!KMr(kW0zjXo2SiFoH kLN8~t3iGWE|8=58=$f)MBl>~e*<^I~9RFa}4c}7Yz-KKvhLi_dmD&A7Q`z@4nkggZ*dd-nz>2Xm#VX zsQ&_%gPevO8d_r_-lNrv|1!=y6(esnG{XM>5qcUoA19V-XzXd{lZaLR{!5tz4`9#!|AH`Y1>}czw_UY@5Th}Ma|8;W7hkzV9q%WyCB z3w7%X?YT$X>QY>`b(@Xr%U>4!fwVc>7%7vwhN3q8Ip#c2oifIAG(;-lgNAicre#sa z33_|EA`A@PvlF*W43nf@eg`LcTpN2ORA8}mgk@F4ur-zBN5K$B+@TCh{g^LuB`5Fr z0eV1~=h&8*>%-1<=5R)vIZ^n=I(FQ=`dvWXpKDXl8z7io3Y5SbGBv}X>HIahAyF02 z$KN-~f~6uWH0I2#*MD3cc;;~$MEa!^3l3pC$b3%)!$LnVEZ}R8p}IJIHE=%GgZ1(@ z!Nz%z*ulC7z`oTkRI-k?y-5>lQFoC7O$-m03^==8kJ3iF8{&AzO(Y?<;E58@LyS5$ zNpD`$Z67a39>}hEiIbyZ*LUOJ6j1DA-|^WXvWa_x6m(E*&6DPDf|3yv`V-) zl)q+V4gUfPo9w7~{OU!*=vkEEBxj_Ayn3?Jflm7N&}8qheU^F4P#0{rI)7Sjgip3ADtIeX==&98eV{^G1fRQN9r&{mA@bf9E0q7nA#b zbNMfppZnUEg%pU7497sstMCdEV*d4IFb_F6G@7;2Vu>C<~I)}mN4y;q%R{h7r zh)l54Hd;zs$}omq(-ur2;tu}#`9cJ(qp(u8pZO+V`NlGaWJ6No3l`T1z?pF@RV?L` zGM(3R$wbW-nC4F<>;10pM%QxPn}5-#=^CtewEt^glDg^q#}(=9hxDZgF5+(|TKIavk881h6M zosh(z%0!{H4eFLwRN%ZY?-*#!w$u9wyeJAgi@^35dKOyZQcw zcR8&+eb6S!C|_n_=y^5f_NLBdN-w1j!Yyc0J??&38^a=Wm-D_DegAc3iUxV=41|A3 zSb7$*3(FMs>sXA!}rgWzJA{SU#xM<;|KOZTwvqE&t*9QN>G9%eY zH|fd#-eT0-=#!#3HT6|{GRMT)dhzr{HB1ek#VXd+0X;* zuBITe%O&O^tv!LqewjP{B#E9tC1M;*jG1hH^UExh87Gs9NcTl$!jEeW^>^>SeWWpV zF`UM_QtmzT)S)rPs4(p!>zN%zodHK<7(`_izD5rY;#(B~{w{&R8cbw<_$5rh=dKMp~-wDJ*G(xAzgg>$I7Q1*kw$N}2PB*Vq4nw82HEkfEP&*5} ziB`d}W<7sOMz6=vs2&k<_A2Mx;J7?e##>pb2#-^lFOgU!DZE?z@Qe+P5o*N|1(cp% z0q`&G#9%TXFW~~`TdR(ScN=g+bw^VWFVDV|U6+v)bZ3PL| zD8q;K(`b%gF9bTJF`~c!v1!+nW$t#N%;U9sbe4#fo-x|45o+=Q8uYNzG+Z`j7y;sF zOFrXPMK;d$A1Y>Yy|;JIPaO@V&9Y5S>43Uj7z5Od5QgeOi)jo@(&`D7U3Q!@16lg3 zB(nNU$ubFJ)JeR9DR&2-SuzLndq<5uj~X*7`wj^W?DvbO*KI>DyWm?r6KqOY*A!ik zDF0$M>fRwA^uwi2Lr!@a#64w#s?uaar{Ek8Ua~LpHE3Qv#GN!E+fmJOQjmgZ_O+HC zTG^D-T|gdlBFC3wL=ot>ClNW}J1n zIJoXBjF}@zlr9!=w?utolO(iioo-+G@-u|ub_T7OE&@&XXTo6?6hOkt&t;Q>gmVcY zc%OJO9U6_$FPv_us71y z6ynb822a|PjaCIfW*fX4s78#BMVp23Sdg%59F2}=+o}j=yl_QG3gq4g#ztz^+NuPN zat^Czdo8f&nH%1eyMF6Dzr}OY{sG+Jfiah7E9q!9{bOcp7v4Wo!&+nlBceIZ{M)v z*#-ZJv@!g(pD?YekmFaFO5v}jQ>heFF2TjOwq-)}ULDbqI#TTB#4q!Ho*LO;uc#{S zfvOUZclgGx8BAEB3RE>A{+!JM|DYUbtn=qOmEgKYyU3%X9QQK%6HmjD6elp-@NRIC zN^6H3HZC?mh4MOUTiEcSUoGusY!c{|gapuuW-Bi&tBBfXqJ*X{RaMu~2J9IXZSYO= z4Q6mp|v5nlLVo&1_0Z@TMFoV>6TmhY1(8_-0unJ~=`3%c1=h zL3WL7t@p+`XWE=FdM?d^o;yUNLGOZdL2PL7!{>B(jYot1@m;%R;o? zGjhIF7~=wv`lD9yX97(0@e19Rif;F1ToKpQSGm58q6v&Lk+gxWoOx?23n^<_V_zGc znXJQs>e?KxHF<}b44y*KtETlSyX<9~ct!KC^WvL}63$B>e&b?8!>dMi57O9-rXg}) zHenp!j+BL(^i1@4x~gd8{C3Ee;o{CL2{KxcEPD+YCXd5%6on%`?O!?3-xKj^2dNF% zm2;kPOWpVV?QOns1!Z{@Nlb*_bMj71l^jI%`tu8fq+)(d>(V%Je3RFm*Ii{qa0-JA zcoiSWM;qfw3EmMJwC0hV&d`K;zxO8fY9|~#wHt8sZ|=;v$SA6mH$Po?FCYvyZgu=v zFuHS4>=*pr?U;(q2+5sm(r((Tccf)L@tr@7c~#7_rtsUBZJ?C>g?>kXBd%W;wi76! zKF}yFu9T*q41;ThOsrYSSII|IZ8#{zLiogRZt+L&(b0GMWhDs7c0_g--KQhT?`f58 zHuXY9R_!h|i9lCev>ZmhA5Nm*D)I08m}(MS_7{~0aAy1>zsYY$XYpE9Pnj)UV=ePU3x9f>w+_|_H56fb|BjijdBlw$!9DD%@ z22;7nd>o50=$%e@2`|l^&-FRCgO)zWk?4n=1(=()!P*q`+ROE&6#Zg2 zv1GgXFH~?;BqF|{pBqM=Z7h@ATby7}^4djST!=vel57B>@=l>swh{9#lOpU5O(v9=*IvXDnWTA zHDkl}RWO)HpTMd;Lx3jHQuEJ+`{|!HaYOf-GO1i09~PxnB`0kLqz!@+_UP%e2YS;5 zc)ZSTcz;5H)Cq&+*-r|ZY9eJSCsm@}NX9zFnd@UlApctlmApD=zhuIXl|H ztI~kp8eXr|G34(~a!BRDVywcHi4}oLqg1_M%)W)KDqSwqNbnP461t6BFDmL|M?Zo; zY^L*8YVS;|dm&N&O9&wE)QV|->F}`r6y=KIdb~DUgpd!GTu$(rxwF>G5T>e4IxVIm4O-|7wG&ydoYa8gN zM#R8512P&M5VCuGw=AP*xKLIt@+E^Y$Bo&%-k7hi`biY1@SI$-08i=;dN1ytz5bTr z4tzxZ;mAt7>eUNlYbNl+r{lAksh4<=mmF4aHX|fhniAPwA%7kS!4|kOxY=qfs9oZH ztXeqf<%uMNqfiBZNC=FNI!5=8Tol+$;ClOL=IhS17+UuhK40@xvR+M-fh0j{2$}wT z2u*1pe)de({*>*j@ZT=n54G#h8Rf(~z6*aR=Y8LhVPAyT)vg}Bb3yv-iLsRT3zD%~ ztPdUiK$F6Wp_R$zEn=Y<2+!-Iw2H2jvdVo)G$x;Hl{p&B2Z-A@YHO(TS`e}8(|6sM zdeGc$TL7x9MS$FOGsd=#wh^7B(Znp%zh9&GC=g`*61Y7>{ScZp&p*^gI@eK!*55z~ zR|5m8WbU}&jsUwCs@^r}nWO|;CR#+>ivXgWgq|u}=}P0Q9+I&oH;^nbAq~oG+f>nN zP$kV`sR8LqY8I1+rI-`gUOq{2v;DW6klL&c_I)wA@`ES+t}u6;4jGeI;KL(5>@`)q zZl-u^ltB48=P06qz}qC5$!&*=;qnFUD`tz`+-%VHSp|-*?b}LhX;A6So%5Tm6`^fa zm|`!HmQQv@*X`P(P^XHiGG z5EG@x*0pa~X*}&Gdo~1eus(mC^7`?Zm2pYs_qc85rjO;j+%PMqZBv*y4@ObB|J9F@ z;!0zlVi&oBBHSF9&`$N1|JyOAcpuN2yl}w+Q@*xr8+d@>`cBR}trapq9sN}!@D67u z2ju|;?jW3M6ef4&lRSE`+4-!GudORxEaE&1; z`jzk!R~`qy6No@*esqX>TAb#aLlEuT!srF8^tku-xOkH2z}G`zU58ZzjsW+UNo23c zA8x;?hdx$f?!HW7dRq&ve;IqfYsN?`pLx-gK4Mjs2#o(~woE-G09XyIXsJ8(=%Flg zO{OaPSbO;K@z{gI_gu=`A;nu@B4pUJ2C$@2p{Y?K0U}tw$O0u7I$r{ef-8Gl3UpVR zzb8J~irk!jFw6??R) z;{gdd$K8hlwsNs4a`fGkNt_&f4Lhqn1JnVYF=JOSxE z<=6`YqRRIi({Mg4{zJPbX2z&*SlBN3C3Em5*t|0=9I*y}r+sgF`2rqYFeV0=D? zuO>`|6FtwnyGmv&azqaAU-HWnX*aNl{&LPyOtqsfejUy%YJi=pINpfDcQ;##o#_1A zYkN{XhE|+SS)L0$6P3)5H29kGj*J4A>7#W^OY`s^(Xaer=H{Bbu<7-Mq2zh~G+yUK ztL->?DEu;Bw+bxnfoRu7#q}(&qc>0qhs0K>iS#KH{{9{!e_shj7E)s;jgMp=#Dupc z@J`Sox?=HV{_Tf?MN|5F>%?dmzoqS^{$Q6hy%prdgUR~Dw3KdTAByj>4rcf%p7#-% z&1e2W2-}O>F+A1N06#^uCzVW}IVXNhkq?q5cyE_m#A{#X!$k4M^UIFI3pbvaQ9cs8 zyauh#_f%}}NXV7iLGc@#IIw-j!;u$ofF;M!hvdgJ%q%)^re>)*u!zA;K24;$g0Eq4 z#CJg3+GqVTVJsl`wZEqY;EQx4!SJVO%9zkEJ?pQD!;6dEg|w;W>w<(^bV6#8sRf~r z6k;_1t=$3v_21QBq|_p>XQTb~vWEVpw3c!{YXBi*$@K5f|7Ly50()iL_7;LUD{puRwD&8SyyA)vEpHKcbq`k<#?|e z9I=N|vZj9O{Qk2Ee`}Jm=bpiqjg4Jics+3pwR-BFP9H}*P(XOGAW2@Jr{=(+Q{s%n z-?GuKh(Fxu04z1!wCr#Z5HKjvq!1*IgiOb(E|3Ct7zJUU$5Od(n#9A5oOuZn}I!!)~abZ z%melOIvx&l8m`MW@1^lZR#uimQ(x}(HaTE-DHpI~FCE)urg9S|U&Ua#z!3o!%J5FJedA_Y@im_pC&r7PZ|hLvTGiboyTj?#&D;!uI1j_+U8k}* zIbc*WY^US*rwYS?kEPtscNfwIOzTiS_RiI?+2G*KY`pcKZNxK#hh5vk65y2Hp#^+r z!8p8oH~Uj8a~Yve3M@03syo^<|12WDEe;uQ2_1*;Tkf)G;=1Elp*l28TLbCEOk1(l z({C9E0{F0ZC`kkdRK|KZPQ#0=m roq}m2%| + + + + + + + \ No newline at end of file diff --git a/images/springboot.svg b/images/springboot.svg new file mode 100644 index 000000000..ee451deaa --- /dev/null +++ b/images/springboot.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/quickstart.html b/src/quickstart.html index e369724e6..fc89be86f 100644 --- a/src/quickstart.html +++ b/src/quickstart.html @@ -42,29 +42,30 @@ - -
    -
    2
    -
    - Learn more about Azure Cosmos DB - -
    -
    -
    -
    -
    -
    1
    -
    - Open and run a sample .NET Core app -

    - We created a sample .NET Core app connected to your Azure Cosmos DB Emulator instance. Download, - extract, build and run the app. -

    - -
    -
    - -
    -
    2
    -
    - Learn more about Azure Cosmos DB. - -
    -
    +
    -
    +
    1
    @@ -171,33 +102,10 @@

    - -
    -
    2
    -
    - Learn more about Azure Cosmos DB. - -
    -
    +
    -
    +
    1
    @@ -215,41 +123,14 @@

    - -
    -
    2
    -
    - Learn more about Azure Cosmos DB. - -
    -
    +
    -
    +
    1
    - Create a new Python app. + Create a new Python app

    Follow this tutorial @@ -257,42 +138,74 @@

    +
    +
    + +
    -
    2
    +
    1
    - Learn more about Azure Cosmos DB. - + Create a new Go app +

    + Follow this + tutorial + to create a new Go app connected to Azure Cosmos DB. +

    +
    +
    + +
    +
    +
    1
    +
    + Create a new Spring Boot app +

    + Follow this + tutorial + to create a new Spring Boot app connected to Azure Cosmos DB. +

    +
    +
    +
    + + + + From d9436be61b583a25e2847a5dce3115c27ef86af2 Mon Sep 17 00:00:00 2001 From: asier-isayas Date: Mon, 28 Apr 2025 13:29:27 -0400 Subject: [PATCH 18/87] Remove references to old Portal Backend (#2109) * remove old portal backend endpoints * format * fix tests * remove Materialized Views from createResourceTokenTreeNodes * add portal FE back to defaultAllowedBackendEndpoints --------- Co-authored-by: Asier Isayas --- src/Utils/EndpointUtils.ts | 20 -------------------- src/Utils/MessageValidation.test.ts | 1 - 2 files changed, 21 deletions(-) diff --git a/src/Utils/EndpointUtils.ts b/src/Utils/EndpointUtils.ts index 885e7f0ff..7e09c923d 100644 --- a/src/Utils/EndpointUtils.ts +++ b/src/Utils/EndpointUtils.ts @@ -52,22 +52,10 @@ export const allowedAadEndpoints: ReadonlyArray = [ ]; export const defaultAllowedBackendEndpoints: ReadonlyArray = [ - "https://main.documentdb.ext.azure.com", - "https://main.documentdb.ext.azure.cn", - "https://main.documentdb.ext.azure.us", - "https://main.cosmos.ext.azure", "https://localhost:12901", "https://localhost:1234", ]; -export const PortalBackendIPs: { [key: string]: string[] } = { - "https://main.documentdb.ext.azure.com": ["104.42.195.92", "40.76.54.131"], - // DE doesn't talk to prod2 (main2) but it might be added - //"https://main2.documentdb.ext.azure.com": ["104.42.196.69"], - "https://main.documentdb.ext.azure.cn": ["139.217.8.252"], - "https://main.documentdb.ext.azure.us": ["52.244.48.71"], -}; - export const PortalBackendOutboundIPs: { [key: string]: string[] } = { [PortalBackendEndpoints.Mpac]: ["13.91.105.215", "4.210.172.107"], [PortalBackendEndpoints.Prod]: ["13.88.56.148", "40.91.218.243"], @@ -98,14 +86,6 @@ export const defaultAllowedCassandraProxyEndpoints: ReadonlyArray = [ CassandraProxyEndpoints.Mooncake, ]; -export const allowedCassandraProxyEndpoints_ToBeDeprecated: ReadonlyArray = [ - "https://main.documentdb.ext.azure.com", - "https://main.documentdb.ext.azure.cn", - "https://main.documentdb.ext.azure.us", - "https://main.cosmos.ext.azure", - "https://localhost:12901", -]; - export const CassandraProxyOutboundIPs: { [key: string]: string[] } = { [CassandraProxyEndpoints.Mpac]: ["40.113.96.14", "104.42.11.145"], [CassandraProxyEndpoints.Prod]: ["137.117.230.240", "168.61.72.237"], diff --git a/src/Utils/MessageValidation.test.ts b/src/Utils/MessageValidation.test.ts index f62396a87..b18860f1f 100644 --- a/src/Utils/MessageValidation.test.ts +++ b/src/Utils/MessageValidation.test.ts @@ -17,7 +17,6 @@ describe("isInvalidParentFrameOrigin", () => { ${"https://cdb-ff-prod-pbe.cosmos.azure.us"} | ${false} ${"https://cdb-mc-prod-pbe.cosmos.azure.cn"} | ${false} ${"https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de"} | ${false} - ${"https://main.documentdb.ext.microsoftazure.de"} | ${false} ${"https://random.domain"} | ${true} ${"https://malicious.cloudapp.azure.com"} | ${true} ${"https://malicious.germanycentral.cloudapp.microsoftazure.de"} | ${true} From 274c85d2deb8d21cc650f0292fed55e76b8c83e2 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Mon, 28 Apr 2025 13:42:18 -0500 Subject: [PATCH 19/87] Added document test skips (#2120) --- test/mongo/document.spec.ts | 1 + test/sql/document.spec.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/test/mongo/document.spec.ts b/test/mongo/document.spec.ts index 3030d5259..34d27ee21 100644 --- a/test/mongo/document.spec.ts +++ b/test/mongo/document.spec.ts @@ -9,6 +9,7 @@ let documentsTab: DocumentsTab = null!; for (const { name, databaseId, containerId, documents } of documentTestCases) { test.describe(`Test MongoRU Documents with ${name}`, () => { + test.skip(true, "Temporarily disabling all tests in this spec file"); test.beforeEach("Open documents tab", async ({ page }) => { explorer = await DataExplorer.open(page, TestAccount.MongoReadonly); diff --git a/test/sql/document.spec.ts b/test/sql/document.spec.ts index 74e3f5da1..095b47c6a 100644 --- a/test/sql/document.spec.ts +++ b/test/sql/document.spec.ts @@ -9,6 +9,7 @@ let documentsTab: DocumentsTab = null!; for (const { name, databaseId, containerId, documents } of documentTestCases) { test.describe(`Test SQL Documents with ${name}`, () => { + test.skip(true, "Temporarily disabling all tests in this spec file"); test.beforeEach("Open documents tab", async ({ page }) => { explorer = await DataExplorer.open(page, TestAccount.SQLReadOnly); From 2f858ecf9b5544e4f25136320fc1e9b48c076f64 Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Tue, 29 Apr 2025 17:50:20 +0200 Subject: [PATCH 20/87] Fabric native improvements: Settings pane, Partition Key settings tab, sample data and message contract (#2119) * Hide entire Accordion of options in Settings Pane * In PartitionKeyComponent hide "Change partition key" label when read-only. * Create sample data container with correct pkey * Add unit tests to PartitionKeyComponent * Fix format * fix unit test snapshot * Add Fabric message to open Settings to given tab id * Improve syntax on message contract * Remove "(preview)" in partition key tab title in Settings Tab --- src/Contracts/DataExplorerMessagesContract.ts | 9 +- src/Contracts/FabricMessageTypes.ts | 1 + .../PartitionKeyComponent.test.tsx | 41 + .../PartitionKeyComponent.tsx | 2 +- .../PartitionKeyComponent.test.tsx.snap | 196 ++++ .../Controls/Settings/SettingsUtils.tsx | 3 +- .../Panes/SettingsPane/SettingsPane.tsx | 838 +++++++++--------- src/Explorer/SplashScreen/SampleUtil.ts | 8 + 8 files changed, 677 insertions(+), 421 deletions(-) create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.test.tsx create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/PartitionKeyComponent.test.tsx.snap diff --git a/src/Contracts/DataExplorerMessagesContract.ts b/src/Contracts/DataExplorerMessagesContract.ts index a38940120..c017bffa8 100644 --- a/src/Contracts/DataExplorerMessagesContract.ts +++ b/src/Contracts/DataExplorerMessagesContract.ts @@ -18,10 +18,13 @@ export type DataExploreMessageV3 = | { type: FabricMessageTypes.GetAllResourceTokens; id: string; + } + | { + type: FabricMessageTypes.OpenSettings; + settingsId: string; }; - -export type GetCosmosTokenMessageOptions = { +export interface GetCosmosTokenMessageOptions { verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace"; resourceType: "" | "dbs" | "colls" | "docs" | "sprocs" | "pkranges"; resourceId: string; -}; +} diff --git a/src/Contracts/FabricMessageTypes.ts b/src/Contracts/FabricMessageTypes.ts index 1d4576391..02871ca47 100644 --- a/src/Contracts/FabricMessageTypes.ts +++ b/src/Contracts/FabricMessageTypes.ts @@ -6,6 +6,7 @@ export enum FabricMessageTypes { GetAllResourceTokens = "GetAllResourceTokens", GetAccessToken = "GetAccessToken", Ready = "Ready", + OpenSettings = "OpenSettings", } export interface AuthorizationToken { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.test.tsx new file mode 100644 index 000000000..dfde4ec78 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.test.tsx @@ -0,0 +1,41 @@ +import { shallow } from "enzyme"; +import { + PartitionKeyComponent, + PartitionKeyComponentProps, +} from "Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent"; +import Explorer from "Explorer/Explorer"; +import React from "react"; + +describe("PartitionKeyComponent", () => { + // Create a test setup function to get fresh instances for each test + const setupTest = () => { + // Create an instance of the mocked Explorer + const explorer = new Explorer(); + // Create minimal mock objects for database and collection + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockDatabase = {} as any as import("../../../../Contracts/ViewModels").Database; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockCollection = {} as any as import("../../../../Contracts/ViewModels").Collection; + + // Create props with the mocked Explorer instance + const props: PartitionKeyComponentProps = { + database: mockDatabase, + collection: mockCollection, + explorer, + }; + + return { explorer, props }; + }; + + it("renders default component and matches snapshot", () => { + const { props } = setupTest(); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it("renders read-only component and matches snapshot", () => { + const { props } = setupTest(); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx index c6a1bd9d1..89810bcb6 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/PartitionKeyComponent.tsx @@ -161,7 +161,7 @@ export const PartitionKeyComponent: React.FC = ({ return ( - Change {partitionKeyName.toLowerCase()} + {!isReadOnly && Change {partitionKeyName.toLowerCase()}} Current {partitionKeyName.toLowerCase()} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/PartitionKeyComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/PartitionKeyComponent.test.tsx.snap new file mode 100644 index 000000000..95d87da3d --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/__snapshots__/PartitionKeyComponent.test.tsx.snap @@ -0,0 +1,196 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PartitionKeyComponent renders default component and matches snapshot 1`] = ` + + + + Change + partition key + + + + + Current + partition key + + + Partitioning + + + + + + Non-hierarchical + + + + + + To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to the source container for the entire duration of the partition key change process. + + Learn more + + + + To change the partition key, a new destination container must be created or an existing destination container selected. Data will then be copied to the destination container. + + + +`; + +exports[`PartitionKeyComponent renders read-only component and matches snapshot 1`] = ` + + + + + + Current + partition key + + + Partitioning + + + + + + Non-hierarchical + + + + + +`; diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index 448b59370..2617af6ac 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -1,6 +1,7 @@ import * as Constants from "../../../Common/Constants"; import * as DataModels from "../../../Contracts/DataModels"; import * as ViewModels from "../../../Contracts/ViewModels"; +import { isFabricNative } from "../../../Platform/Fabric/FabricUtil"; import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; const zeroValue = 0; @@ -165,7 +166,7 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => { case SettingsV2TabTypes.IndexingPolicyTab: return "Indexing Policy"; case SettingsV2TabTypes.PartitionKeyTab: - return "Partition Keys (preview)"; + return isFabricNative() ? "Partition Keys" : "Partition Keys (preview)"; case SettingsV2TabTypes.ComputedPropertiesTab: return "Computed Properties"; case SettingsV2TabTypes.ContainerVectorPolicyTab: diff --git a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx index a40f4da99..ca92b59ed 100644 --- a/src/Explorer/Panes/SettingsPane/SettingsPane.tsx +++ b/src/Explorer/Panes/SettingsPane/SettingsPane.tsx @@ -23,7 +23,7 @@ import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { Platform, configContext } from "ConfigContext"; import { useDialog } from "Explorer/Controls/Dialog"; import { useDatabases } from "Explorer/useDatabases"; -import { isFabric } from "Platform/Fabric/FabricUtil"; +import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil"; import { AppStateComponentNames, deleteAllStates, @@ -607,441 +607,447 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ return (
    - - {shouldShowQueryPageOptions && ( - - -
    Page Options
    -
    - -
    -
    - Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as - many query results per page. -
    - -
    -
    - {isCustomPageOptionSelected() && ( -
    -
    - Query results per page{" "} - - Enter the number of query results that should be shown per page. - -
    - - { - setCustomItemPerPage(parseInt(newValue) + 1 || customItemPerPage); - }} - onDecrement={(newValue) => setCustomItemPerPage(parseInt(newValue) - 1 || customItemPerPage)} - onValidate={(newValue) => setCustomItemPerPage(parseInt(newValue) || customItemPerPage)} - min={1} - step={1} - className="textfontclr" - incrementButtonAriaLabel="Increase value by 1" - decrementButtonAriaLabel="Decrease value by 1" - /> -
    - )} -
    -
    -
    - )} - {showEnableEntraIdRbac && ( - - -
    Enable Entra ID RBAC
    -
    - -
    -
    - Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID - RBAC. - - {" "} - Learn more{" "} - -
    - -
    -
    -
    - )} - {userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && ( - - -
    Region Selection
    -
    - -
    -
    - Changes region the Cosmos Client uses to access account. -
    -
    - Select Region - - Changes the account endpoint used to perform client operations. - -
    - option.key === selectedRegionalEndpoint)?.text - : regionOptions[0]?.text - } - onChange={handleOnSelectedRegionOptionChange} - options={regionOptions} - styles={{ root: { marginBottom: "10px" } }} - /> -
    -
    -
    - )} - {userContext.apiType === "SQL" && !isEmulator && ( - <> - + {!isFabricNative() && ( + + {shouldShowQueryPageOptions && ( + -
    Query Timeout
    +
    Page Options
    - When a query reaches a specified time limit, a popup with an option to cancel the query will show - unless automatic cancellation has been enabled. -
    - -
    - {queryTimeoutEnabled && ( -
    - - -
    - )} -
    -
    - - - -
    RU Limit
    -
    - -
    -
    - If a query exceeds a configured RU limit, the query will be aborted. -
    - -
    - {ruThresholdEnabled && ( -
    - -
    - )} -
    -
    - - - -
    Default Query Results View
    -
    - -
    -
    - Select the default view to use when displaying query results. + Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as + many query results per page.
    +
    +
    + {isCustomPageOptionSelected() && ( +
    +
    + Query results per page{" "} + + Enter the number of query results that should be shown per page. + +
    + + { + setCustomItemPerPage(parseInt(newValue) + 1 || customItemPerPage); + }} + onDecrement={(newValue) => setCustomItemPerPage(parseInt(newValue) - 1 || customItemPerPage)} + onValidate={(newValue) => setCustomItemPerPage(parseInt(newValue) || customItemPerPage)} + min={1} + step={1} + className="textfontclr" + incrementButtonAriaLabel="Increase value by 1" + decrementButtonAriaLabel="Decrease value by 1" + /> +
    + )} +
    +
    +
    + )} + {showEnableEntraIdRbac && ( + + +
    Enable Entra ID RBAC
    +
    + +
    +
    + Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra + ID RBAC. + + {" "} + Learn more{" "} + +
    +
    - - )} + )} + {userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && ( + + +
    Region Selection
    +
    + +
    +
    + Changes region the Cosmos Client uses to access account. +
    +
    + Select Region + + Changes the account endpoint used to perform client operations. + +
    + option.key === selectedRegionalEndpoint)?.text + : regionOptions[0]?.text + } + onChange={handleOnSelectedRegionOptionChange} + options={regionOptions} + styles={{ root: { marginBottom: "10px" } }} + /> +
    +
    +
    + )} + {userContext.apiType === "SQL" && !isEmulator && ( + <> + + +
    Query Timeout
    +
    + +
    +
    + When a query reaches a specified time limit, a popup with an option to cancel the query will + show unless automatic cancellation has been enabled. +
    + +
    + {queryTimeoutEnabled && ( +
    + + +
    + )} +
    +
    - {showRetrySettings && ( - - -
    Retry Settings
    -
    - -
    -
    - Retry policy associated with throttled requests during CosmosDB queries. + + +
    RU Limit
    +
    + +
    +
    + If a query exceeds a configured RU limit, the query will be aborted. +
    + +
    + {ruThresholdEnabled && ( +
    + +
    + )} +
    +
    + + + +
    Default Query Results View
    +
    + +
    +
    + Select the default view to use when displaying query results. +
    + +
    +
    +
    + + )} + + {showRetrySettings && ( + + +
    Retry Settings
    +
    + +
    +
    + Retry policy associated with throttled requests during CosmosDB queries. +
    +
    + Max retry attempts + + Max number of retries to be performed for a request. Default value 9. + +
    + setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)} + onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)} + onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)} + styles={spinButtonStyles} + /> +
    + Fixed retry interval (ms) + + Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned + as part of the response. Default value is 0 milliseconds. + +
    + setRetryInterval(parseInt(newValue) + 1000 || retryInterval)} + onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)} + onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)} + styles={spinButtonStyles} + /> +
    + Max wait time (s) + + Max wait time in seconds to wait for a request while the retries are happening. Default value 30 + seconds. + +
    + + setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds) + } + onDecrement={(newValue) => + setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds) + } + onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)} + styles={spinButtonStyles} + />
    -
    - Max retry attempts - - Max number of retries to be performed for a request. Default value 9. - + + + )} + {!isEmulator && ( + + +
    Enable container pagination
    +
    + +
    +
    + Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order. +
    + setContainerPaginationEnabled(!containerPaginationEnabled)} + label="Enable container pagination" + />
    - setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)} - onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)} - onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)} - styles={spinButtonStyles} - /> -
    - Fixed retry interval (ms) - - Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned - as part of the response. Default value is 0 milliseconds. - + + + )} + {shouldShowCrossPartitionOption && ( + + +
    Enable cross-partition query
    +
    + +
    +
    + Send more than one request while executing a query. More than one request is necessary if the + query is not scoped to single partition key value. +
    + setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)} + label="Enable cross-partition query" + />
    - setRetryInterval(parseInt(newValue) + 1000 || retryInterval)} - onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)} - onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)} - styles={spinButtonStyles} - /> -
    - Max wait time (s) - - Max wait time in seconds to wait for a request while the retries are happening. Default value 30 - seconds. - + + + )} + {shouldShowParallelismOption && ( + + +
    Max degree of parallelism
    +
    + +
    +
    + Gets or sets the number of concurrent operations run client side during parallel query execution. + A positive property value limits the number of concurrent operations to the set value. If it is + set to less than 0, the system automatically decides the number of concurrent operations to run. +
    + + setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism) + } + onDecrement={(newValue) => + setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism) + } + onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)} + ariaLabel="Max degree of parallelism" + label="Max degree of parallelism" + />
    - setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)} - onDecrement={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) - 1 || MaxWaitTimeInSeconds)} - onValidate={(newValue) => setMaxWaitTimeInSeconds(parseInt(newValue) || MaxWaitTimeInSeconds)} - styles={spinButtonStyles} - /> -
    -
    -
    - )} - {!isEmulator && ( - - -
    Enable container pagination
    -
    - -
    -
    - Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order. + + + )} + {shouldShowPriorityLevelOption && ( + + +
    Priority Level
    +
    + +
    +
    + Sets the priority level for data-plane requests from Data Explorer when using Priority-Based + Execution. If "None" is selected, Data Explorer will not specify priority level, and the + server-side default priority level will be used. +
    +
    - setContainerPaginationEnabled(!containerPaginationEnabled)} - label="Enable container pagination" - /> -
    - - - )} - {shouldShowCrossPartitionOption && ( - - -
    Enable cross-partition query
    -
    - -
    -
    - Send more than one request while executing a query. More than one request is necessary if the query - is not scoped to single partition key value. + + + )} + {shouldShowGraphAutoVizOption && ( + + +
    Display Gremlin query results as: 
    +
    + +
    +
    + Select Graph to automatically visualize the query results as a Graph or JSON to display the + results as JSON. +
    +
    - setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)} - label="Enable cross-partition query" - /> -
    - - - )} - {shouldShowParallelismOption && ( - - -
    Max degree of parallelism
    -
    - -
    -
    - Gets or sets the number of concurrent operations run client side during parallel query execution. A - positive property value limits the number of concurrent operations to the set value. If it is set to - less than 0, the system automatically decides the number of concurrent operations to run. + + + )} + {shouldShowCopilotSampleDBOption && ( + + +
    Enable sample database
    +
    + +
    +
    + This is a sample database and collection with synthetic product data you can use to explore using + NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and + is created by, and maintained by Microsoft at no cost to you. +
    +
    - - setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism) - } - onDecrement={(newValue) => - setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism) - } - onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)} - ariaLabel="Max degree of parallelism" - label="Max degree of parallelism" - /> -
    - - - )} - {shouldShowPriorityLevelOption && ( - - -
    Priority Level
    -
    - -
    -
    - Sets the priority level for data-plane requests from Data Explorer when using Priority-Based - Execution. If "None" is selected, Data Explorer will not specify priority level, and the - server-side default priority level will be used. -
    - -
    -
    -
    - )} - {shouldShowGraphAutoVizOption && ( - - -
    Display Gremlin query results as: 
    -
    - -
    -
    - Select Graph to automatically visualize the query results as a Graph or JSON to display the results - as JSON. -
    - -
    -
    -
    - )} - {shouldShowCopilotSampleDBOption && ( - - -
    Enable sample database
    -
    - -
    -
    - This is a sample database and collection with synthetic product data you can use to explore using - NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and - is created by, and maintained by Microsoft at no cost to you. -
    - -
    -
    -
    - )} - + + + )} + + )}
    diff --git a/src/Explorer/SplashScreen/SampleUtil.ts b/src/Explorer/SplashScreen/SampleUtil.ts index 837227c8f..4072eb011 100644 --- a/src/Explorer/SplashScreen/SampleUtil.ts +++ b/src/Explorer/SplashScreen/SampleUtil.ts @@ -1,3 +1,4 @@ +import { BackendDefaults } from "Common/Constants"; import { createCollection } from "Common/dataAccess/createCollection"; import Explorer from "Explorer/Explorer"; import { useDatabases } from "Explorer/useDatabases"; @@ -35,6 +36,11 @@ export const createContainer = async ( collectionId: containerName, databaseId: databaseName, databaseLevelThroughput: false, + partitionKey: { + paths: [`/${SAMPLE_DATA_PARTITION_KEY}`], + kind: "Hash", + version: BackendDefaults.partitionKeyVersion, + }, }; await createCollection(createRequest); await explorer.refreshAllDatabases(); @@ -47,6 +53,8 @@ export const createContainer = async ( return newCollection; }; +const SAMPLE_DATA_PARTITION_KEY = "category"; // This pkey is specifically set for queryCopilotSampleData.json below + export const importData = async (collection: ViewModels.Collection): Promise => { // TODO: keep same chunk as ContainerSampleGenerator const dataFileContent = await import( From 9f3236c29ca38c44ec9314345b0be24f1858dcfb Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:35:58 +0530 Subject: [PATCH 21/87] [accessibility-3560183]:[Screen reader - Cosmos DB Query Copilot - Query Faster with Copilot>Enable Query Advisor]: Screen reader does not announce the dialog information on invoking 'Clear editor' button. (#2068) Co-authored-by: Satyapriya Bai --- src/Explorer/QueryCopilot/Popup/DeletePopup.tsx | 11 ++++++++--- .../Popup/__snapshots__/DeletePopup.test.tsx.snap | 8 ++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Explorer/QueryCopilot/Popup/DeletePopup.tsx b/src/Explorer/QueryCopilot/Popup/DeletePopup.tsx index 2a4e29641..6e2fbed54 100644 --- a/src/Explorer/QueryCopilot/Popup/DeletePopup.tsx +++ b/src/Explorer/QueryCopilot/Popup/DeletePopup.tsx @@ -22,12 +22,17 @@ export const DeletePopup = ({ }; return ( - + - + Delete code? - + This will clear the query from the query builder pane along with all comments and also reset the prompt pane diff --git a/src/Explorer/QueryCopilot/Popup/__snapshots__/DeletePopup.test.tsx.snap b/src/Explorer/QueryCopilot/Popup/__snapshots__/DeletePopup.test.tsx.snap index 698138f5a..988fd88db 100644 --- a/src/Explorer/QueryCopilot/Popup/__snapshots__/DeletePopup.test.tsx.snap +++ b/src/Explorer/QueryCopilot/Popup/__snapshots__/DeletePopup.test.tsx.snap @@ -11,6 +11,8 @@ exports[`Delete Popup snapshot test should not render when showDeletePopup is fa }, } } + subtitleAriaId="deleteDialogSubTitle" + titleAriaId="deleteDialogTitle" > Date: Wed, 30 Apr 2025 13:32:53 +0530 Subject: [PATCH 22/87] Emulator Quickstart Tutorials (#2121) * updated all outdated sample apps Co-authored-by: nishthaAhujaa --- less/quickstart.less | 1322 +++++++++++++++++++++--------------------- src/quickstart.html | 62 +- 2 files changed, 688 insertions(+), 696 deletions(-) diff --git a/less/quickstart.less b/less/quickstart.less index dd9b2e33a..539d63448 100644 --- a/less/quickstart.less +++ b/less/quickstart.less @@ -1,927 +1,923 @@ @import "./Common/Constants"; html { - font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; - padding: 0px; - margin: 0px; - overflow: hidden; + font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; + padding: 0px; + margin: 0px; + overflow: hidden; } body { - font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; - font-size: 12px; + font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; + font-size: 12px; } .fixedleftpane { - background: #2f2d2d; - height: 100vh; - width: 80px; - float: left; + background: #2f2d2d; + height: 100vh; + width: 80px; + float: left; } #divQuickStart, #divExplorer { - display: inline-block; - width: 100%; - white-space: nowrap; + display: inline-block; + width: 100%; + white-space: nowrap; } #imgiconwidth1 { - width: 72%; + width: 72%; } #Quickstart { - text-align: center; - width: 80px; - height: 60px; - margin: 0 auto; - padding-top: 5px; - position: relative; + text-align: center; + width: 80px; + height: 60px; + margin: 0 auto; + padding-top: 5px; + position: relative; } .collectionheading { - text-transform: uppercase; - font-size: 10px; + text-transform: uppercase; + font-size: 10px; } #Quickstart #imgiconwidth1 { - width: 24px; - height: 24px; - position: absolute; - right: 27px; + width: 24px; + height: 24px; + position: absolute; + right: 27px; } .topSelected { - border-left: 4px solid @AccentMediumHigh; - background: #666666; + border-left: 4px solid @AccentMediumHigh; + background: #666666; } .topSelected:hover { - border-left: 4px solid @AccentMediumHigh; - background: #666666!important; - cursor: default!important; + border-left: 4px solid @AccentMediumHigh; + background: #666666 !important; + cursor: default !important; } #Quickstart:hover span.activemenu, #Quickstart:active span.activemenu { - color: #fff; + color: #fff; } #Explorer:hover span.menuExplorer, #Explorer:active span.menuExplorer { - color: #fff; + color: #fff; } menuQuickStart { - margin-left: 0; - padding-left: 0; - display: block; - right: 12px; - top: 30px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 12px; + top: 30px; + position: absolute; } #Explorer { - text-align: center; - display: inline-block; - width: 80px; - height: 60px; - margin: 0 auto; - padding-top: 9px; - position: relative; + text-align: center; + display: inline-block; + width: 80px; + height: 60px; + margin: 0 auto; + padding-top: 9px; + position: relative; } #Explorer #imgiconwidth1, .feedbackstyle #imgiconwidth1, .settingstyle #imgiconwidth1 { - width: 24px; - height: 24px; - position: absolute; - right: 30px; + width: 24px; + height: 24px; + position: absolute; + right: 30px; } #Explorer span.menuExplorer { - margin-left: 0; - padding-left: 0; - display: block; - right: 19px; - top: 33px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 19px; + top: 33px; + position: absolute; } .feedbackstyle span.menuExplorer { - margin-left: 0; - padding-left: 0; - display: block; - right: 19px; - top: 33px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 19px; + top: 33px; + position: absolute; } .settingstyle span.menuExplorer { - margin-left: 0; - padding-left: 0; - display: block; - right: 19px; - top: 33px; - position: absolute; + margin-left: 0; + padding-left: 0; + display: block; + right: 19px; + top: 33px; + position: absolute; } .content { - display: inline-block; - width: 100%; - transition: all .4s ease-in-out; - -ms-transition: all .4s ease-in-out; - -webkit-transition: all .4s ease-in-out; - -moz-transition: all .4s ease-in-out; - height: 100vh; + display: inline-block; + width: 100%; + transition: all 0.4s ease-in-out; + -ms-transition: all 0.4s ease-in-out; + -webkit-transition: all 0.4s ease-in-out; + -moz-transition: all 0.4s ease-in-out; + height: 100vh; } .mini { - width: 0%; - float: left; - transition: all .4s ease-in-out; - -webkit-transition: all .4s ease-in-out; - -moz-transition: all .4s ease-in-out; - height: 100vh; - background-color: white; + width: 0%; + float: left; + transition: all 0.4s ease-in-out; + -webkit-transition: all 0.4s ease-in-out; + -moz-transition: all 0.4s ease-in-out; + height: 100vh; + background-color: white; } #sidebar-wrapper { - z-index: 1000; - position: fixed; - left: 250px; - width: 0; - height: 100%; - margin-left: -250px; - overflow-y: auto; - background: white; - -webkit-transition: all 0.5s ease; - -moz-transition: all 0.5s ease; - -o-transition: all 0.5s ease; - transition: all 0.5s ease; + z-index: 1000; + position: fixed; + left: 250px; + width: 0; + height: 100%; + margin-left: -250px; + overflow-y: auto; + background: white; + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + -o-transition: all 0.5s ease; + transition: all 0.5s ease; } .toggle-left { - width: 0%; - overflow: hidden; + width: 0%; + overflow: hidden; } .toggle-minicontent { - width: 100%; + width: 100%; } .toggle-maincontent { - width: 100%; + width: 100%; } .toggle-mini { - width: 50px; + width: 50px; } .toggle-main { - width: 100%; + width: 100%; } .activepartitionmode { - background-color: @AccentMediumHigh; + background-color: @AccentMediumHigh; } .paddingpartition { - color: white; - padding-left: 15px; - padding-top: 25px; + color: white; + padding-left: 15px; + padding-top: 25px; } .paddingspan2 { - padding-top: 20px; - color: #000; - padding-left: 15px; + padding-top: 20px; + color: #000; + padding-left: 15px; } .paddingspan4 { - padding-top: 20px; - padding-bottom: 20px; - color: white; - padding-left: 15px; + padding-top: 20px; + padding-bottom: 20px; + color: white; + padding-left: 15px; } .whitegroove { - width: 344px; - border: groove; + width: 344px; + border: groove; } .dropdownbtn { - color: white; - width: 340px; - background: #262626; + color: white; + width: 340px; + background: #262626; } .queryclr { - color: white; - background: #262626; + color: white; + background: #262626; } .pointer { - cursor: pointer; + cursor: pointer; } -#tbodycontent>tr>td { - border-bottom: 1px solid #cccccc; +#tbodycontent > tr > td { + border-bottom: 1px solid #cccccc; } -#tbodycontent>tr:last-child>td { - border-bottom: 1px solid #ddd; +#tbodycontent > tr:last-child > td { + border-bottom: 1px solid #ddd; } .gridRowSelected { - background-color: #DEF; + background-color: #def; } .gridRowSelected:hover { - background-color: #DEF!important; - cursor: initial; + background-color: #def !important; + cursor: initial; } .collectionNodeSelected { - background-color: #DEF; + background-color: #def; } .collectionNodeSelected:hover { - background-color: #DEF!important; - cursor: default!important; + background-color: #def !important; + cursor: default !important; } .databaseNodeSelected { - background-color: #DEF; + background-color: #def; } .databaseNodeSelected:hover { - background-color: #DEF!important; - cursor: default!important; + background-color: #def !important; + cursor: default !important; } .leftsidepanle-hr { - margin: 16px 0px; - border-top: 1px solid #eee; - margin-left: -17px; - width: 100%; - color: 1px solid #53575B; + margin: 16px 0px; + border-top: 1px solid #eee; + margin-left: -17px; + width: 100%; + color: 1px solid #53575b; } .partitioning-btn { - padding-bottom: 16px; + padding-bottom: 16px; } .btncreatecoll1 { - border: 1px solid @AccentMediumHigh; - background-color: @AccentMediumHigh; - color: #fff; - padding: 2px 30px; - cursor: pointer; - font-size: 12px; + border: 1px solid @AccentMediumHigh; + background-color: @AccentMediumHigh; + color: #fff; + padding: 2px 30px; + cursor: pointer; + font-size: 12px; } .btncreatecoll1:hover { - background: @AccentMediumHigh; - color: #fff; - border-color: @AccentMediumHigh; - cursor: pointer; - font-size: 12px; + background: @AccentMediumHigh; + color: #fff; + border-color: @AccentMediumHigh; + cursor: pointer; + font-size: 12px; } .btncreatecoll1:active { - border: 1px solid #0072c6; - background-color: #0072c6; - color: white; - padding: 2px 30px; - cursor: pointer; - font-size: 12px; + border: 1px solid #0072c6; + background-color: #0072c6; + color: white; + padding: 2px 30px; + cursor: pointer; + font-size: 12px; } .btncreatecoll1-off { - border: 1px solid #969696; - background-color: #000; - color: white; - padding: 2px 30px; - cursor: pointer; - font-size: 12px; - margin-left: -5px; + border: 1px solid #969696; + background-color: #000; + color: white; + padding: 2px 30px; + cursor: pointer; + font-size: 12px; + margin-left: -5px; } .leftpanel-okbut { - padding: 20px 0px 24px 30px; + padding: 20px 0px 24px 30px; } .btnpricepad { - margin-left: 24px; + margin-left: 24px; } .collid { - background: #fff; - width: calc(~"100% - 80px"); + background: #fff; + width: calc(~"100% - 80px"); } .textfontclr { - color: #000; + color: #000; } .collid-white { - width: 100%; - border: solid 1px #DDD; + width: 100%; + border: solid 1px #ddd; } .plusimg-but { - background-image: url(../images/plus_normal.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_normal.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .plusimg-but:hover { - background-image: url(../images/plus_hover.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_hover.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .plusimg-but:active { - background-image: url(../images/plus_pressed.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_pressed.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .plusimg-but:disabled { - background-image: url(../images/plus_disabled.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/plus_disabled.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but { - background-image: url(../images/minus_normal.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_normal.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but:hover { - background-image: url(../images/minus_hover.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_hover.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but:active { - background-image: url(../images/minus_pressed.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_pressed.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .minusimg-but:disabled { - background-image: url(../images/minus_disabled.svg); - background-repeat: no-repeat; - padding: 6px 16px; - position: static; - padding-top: 4px; + background-image: url(../images/minus_disabled.svg); + background-repeat: no-repeat; + padding: 6px 16px; + position: static; + padding-top: 4px; } .firstdivbg { - padding: @MediumSpace 0px @DefaultSpace (2 * @LargeSpace); - background-color: @BaseLight; + padding: @MediumSpace 0px @DefaultSpace (2 * @LargeSpace); + background-color: @BaseLight; } p { - margin: 0 0 4px; - color: #000; + margin: 0 0 4px; + color: #000; } .closeImg { - float: right; - margin: 0px 20px 0px 0px; - cursor: pointer; - padding: 6px 20px 20px 6px; - width: 20px; - height: 20px; + float: right; + margin: 0px 20px 0px 0px; + cursor: pointer; + padding: 6px 20px 20px 6px; + width: 20px; + height: 20px; } .seconddivpadding { - padding-top: 16px; + padding-top: 16px; } .seconddivbg { - height: 100vh; - padding-left: 32px; - padding-top: 16px; + height: 100vh; + padding-left: 32px; + padding-top: 16px; } .pkPadding { - padding-top: 12px; + padding-top: 12px; } .mandatoryStar { - color: #ff0707; - font-size: 14px; - font-weight: bold; + color: #ff0707; + font-size: 14px; + font-weight: bold; } .pricingtierimg { - padding-left: 20px; - padding-top: 10px; - padding-bottom: 20px; + padding-left: 20px; + padding-top: 10px; + padding-bottom: 20px; } .headerline { - color: @BaseDark; - font-size: 16px; - border-bottom: 1px solid @BaseMedium; + color: @BaseDark; + font-size: 16px; + border-bottom: 1px solid @BaseMedium; } .partitionkeystyle { - font-size: 10px; + font-size: 10px; } .arrowprice { - margin-left: 230px; + margin-left: 230px; } .paddingspan { - padding: 20px; - color: white; - font-size: 14px; + padding: 20px; + color: white; + font-size: 14px; } input::-webkit-calendar-picker-indicator { - opacity: 100; + opacity: 100; } .paddingspan3 { - color: white; - font-size: 14px; - position: absolute; - width: 100%; - height: 100px; - bottom: 150px; + color: white; + font-size: 14px; + position: absolute; + width: 100%; + height: 100px; + bottom: 150px; } .paddingspan4 { - padding-top: 20px; - padding-left: 20px; - color: white; - font-size: 14px; + padding-top: 20px; + padding-left: 20px; + color: white; + font-size: 14px; } .closebtnn { - float: right; - padding: 0 10px; - cursor: pointer; + float: right; + padding: 0 10px; + cursor: pointer; } label { - white-space: nowrap; - font: 12px "Segoe UI"; + white-space: nowrap; + font: 12px "Segoe UI"; } .Introlines { - padding-top: 27px; - padding-left: 25px; + padding-top: 27px; + padding-left: 25px; } .Introline1 { - font-size: 16px; + font-size: 16px; } .Introline2 { - font-size: 14px; - padding-top: 10px; + font-size: 14px; + padding-top: 10px; } .datalist-arrow { - position: relative; + position: relative; } .datalist-arrow:hover:after { - background: #969696; + background: #969696; } .datalist-arrow:focus:after, .datalist-arrow:active:after { - background: #1EBBEE; + background: #1ebbee; } input::-webkit-calendar-picker-indicator::after { - content: '\276F'; - right: 0; - top: -8%; - display: block; - width: 27px; - height: 25px; - line-height: 25px; - color: #fff; - text-align: center; - pointer-events: none; - transform: rotate(90deg); + content: "\276F"; + right: 0; + top: -8%; + display: block; + width: 27px; + height: 25px; + line-height: 25px; + color: #fff; + text-align: center; + pointer-events: none; + transform: rotate(90deg); } .datalist-arrow:after:hover { - content: '\276F'; - position: absolute; - right: 1px; - top: 6%; - transform: rotate(90deg); - display: block; - width: 27px; - height: 25px; - line-height: 25px; - color: #fff; - text-align: center; - pointer-events: none; - background-color: #1EBBEE; + content: "\276F"; + position: absolute; + right: 1px; + top: 6%; + transform: rotate(90deg); + display: block; + width: 27px; + height: 25px; + line-height: 25px; + color: #fff; + text-align: center; + pointer-events: none; + background-color: #1ebbee; } .Introline3 { - padding-top: 10px; - font-size: 14px; - font-weight: 1000; + padding-top: 10px; + font-size: 14px; + font-weight: 1000; } .collectionCollapsed { - color: black; - font-weight: 400; - font-size: 14px; - position: relative; - display: block; - padding: 8px 15px; - cursor: pointer; - margin-right: 13px; - border: 1px solid #fff; + color: black; + font-weight: 400; + font-size: 14px; + position: relative; + display: block; + padding: 8px 15px; + cursor: pointer; + margin-right: 13px; + border: 1px solid #fff; } .collectionCollapsed:hover { - background: #EEEEEE; + background: #eeeeee; } .collectionCollapsed:active { - border: solid 1px @AccentMediumHigh; + border: solid 1px @AccentMediumHigh; } .collectionCollapsed:focus { - border: Solid 1px @AccentMediumHigh; + border: Solid 1px @AccentMediumHigh; } .arrowCollapsed { - cursor: pointer; - width: 16px; - height: 16px; - transform: rotate(-90deg) translateX(-100%); - -webkit-transform: rotate(-90deg) translateX(-100%); - -ms-transform: rotate(-90deg) translateX(-100%); - margin: -30px 3px 0px 2px; + cursor: pointer; + width: 16px; + height: 16px; + transform: rotate(-90deg) translateX(-100%); + -webkit-transform: rotate(-90deg) translateX(-100%); + -ms-transform: rotate(-90deg) translateX(-100%); + margin: -30px 3px 0px 2px; } .leftarrowCollapsed { - padding: 2px 4px 4px 5px; - border: solid 1px #FFF; - margin: 6px 4px 0px -11px; + padding: 2px 4px 4px 5px; + border: solid 1px #fff; + margin: 6px 4px 0px -11px; } .leftarrowCollapsed:hover { - background-color: #EEEEEE; + background-color: #eeeeee; } .leftarrowCollapsed:active { - border: solid 1px @AccentMediumHigh; + border: solid 1px @AccentMediumHigh; } .leftarrowCollapsed:focus { - border: Solid 1px @AccentMediumHigh; + border: Solid 1px @AccentMediumHigh; } .qslevel { - padding-top: 10px; - padding-left: 10px; - width: 60%; - min-width: 960px; + padding-top: 10px; + padding-left: 10px; + width: 60%; + min-width: 960px; } .nav-tabs-margin { - margin-top: 20px; + margin-top: 20px; } .numbersize { - font-size: 30px; - display: inline; - font-weight: 600; + font-size: 30px; + display: inline; + font-weight: 600; } .numbersizePadding { - padding-right: 5px; + padding-right: 5px; } .numberheading { - display: inline; - position: absolute; - padding-top: 10px; - font-size: 16px; - padding-left: 15px; + display: inline; + position: absolute; + padding-top: 10px; + font-size: 16px; + padding-left: 15px; } -.numberheading>p { - padding-top: 10px; - font-size: 12px; +.numberheading > p { + padding-top: 10px; + font-size: 12px; } -.numberheading>ul { - padding-top: 10px; - padding-left: 0px; - list-style-type: none; +.numberheading > ul { + padding-top: 10px; + padding-left: 0px; + list-style-type: none; } .numberheading ul li { - padding-bottom: 5px; + padding-bottom: 5px; } -.numberheading>ul>li>a { - font-size: 12px; - color: #0058ad; +.numberheading > ul > li > a { + font-size: 12px; + color: #0058ad; } -.netApp { - padding-bottom: 80px; -} - -.pythonApp { - padding-bottom: 45px; +.sampleApp { + padding-bottom: 45px; } .step1 { - padding-bottom: 110px; + padding-bottom: 110px; } -.step1>input { - font-size: 12px; +.step1 > input { + font-size: 12px; } .btncreatecoll { - background: #0058ad; - color: #fff; - padding: 5px 20px; - cursor: pointer; - font-size: 12px; - border: 1px solid #0058ad; + background: #0058ad; + color: #fff; + padding: 5px 20px; + cursor: pointer; + font-size: 12px; + border: 1px solid #0058ad; } .btncreatecoll:hover { - background-color: #0074e0; + background-color: #0074e0; } .atags:focus { - color: @AccentMediumHigh; + color: @AccentMediumHigh; } .atags { - color: @AccentMediumHigh; - font-weight: 400; - cursor: pointer + color: @AccentMediumHigh; + font-weight: 400; + cursor: pointer; } .qsmenuicons { - width: 25px; - height: 25px; - margin-right: 5px; + width: 25px; + height: 25px; + margin-right: 5px; } .HeaderBg { - background-color: #202428; - height: 60px; + background-color: #202428; + height: 60px; } .title { - color: @AccentMediumHigh; - font-size: 20px; - padding-left: 10px; + color: @AccentMediumHigh; + font-size: 20px; + padding-left: 10px; } .items { - padding-left: 24px; - padding-top: 15px; + padding-left: 24px; + padding-top: 15px; } .divmenuquickstartpadding { - padding-left: 24px; - padding-bottom: 8px; + padding-left: 24px; + padding-bottom: 8px; } .menuQuickStart { - font-size: 12px; - color: white; - padding-left: 10px; + font-size: 12px; + color: white; + padding-left: 10px; } .menuExplorer { - font-size: 12px; - color: white; - padding-left: 20px; + font-size: 12px; + color: white; + padding-left: 20px; } .activemenu { - color: #fff; + color: #fff; } .rightarrowimg { - padding-left: 5px; - padding-bottom: 2px; + padding-left: 5px; + padding-bottom: 2px; } a:hover, a:visited, a:active, a:link { - text-decoration: none; + text-decoration: none; } .command { - padding: 8px; + padding: 8px; } .command:hover { - background-color: #E6E6E6; - cursor: pointer; - padding-bottom: 12px; + background-color: #e6e6e6; + cursor: pointer; + padding-bottom: 12px; } .command:active { - background-color: #CCCCCC; - border: solid 1px @AccentMediumHigh; + background-color: #cccccc; + border: solid 1px @AccentMediumHigh; } .command:focus { - padding: 7px 8px 11px 8px; - border: solid 1px @AccentMediumHigh; - outline: none; + padding: 7px 8px 11px 8px; + border: solid 1px @AccentMediumHigh; + outline: none; } -.nav>li>a:focus { - background-color: white; +.nav > li > a:focus { + background-color: white; } .commandIcon { - margin: 0 5px 0 0; - vertical-align: text-top; - width: 18px; - height: 18px; + margin: 0 5px 0 0; + vertical-align: text-top; + width: 18px; + height: 18px; } .iconpadclick { - background-color: #e6e6e6; - cursor: pointer; - border: 1px solid #1ebbee; - padding: 5px; + background-color: #e6e6e6; + cursor: pointer; + border: 1px solid #1ebbee; + padding: 5px; } .divimgleftarrow { - display: inline-block; - margin-top: 16px; - margin-right: 10px; + display: inline-block; + margin-top: 16px; + margin-right: 10px; } .divimgleftarrow:hover { - background-color: #e6e6e6; - cursor: pointer; - border: 1px solid #1ebbee; + background-color: #e6e6e6; + cursor: pointer; + border: 1px solid #1ebbee; } .adddeliconspan { - display: none; - float: right; - padding: 5px; + display: none; + float: right; + padding: 5px; } .spanparent:hover .adddeliconspan { - display: inline; + display: inline; } .spanchild:hover .adddeliconspan { - display: inline; + display: inline; } .collectiontitle { - font-size: 14px; - text-transform: uppercase; + font-size: 14px; + text-transform: uppercase; } .titlepadcol { - padding-left: 20px; - font-weight: 500; - height: 28px; - display: inline-block; - padding-top: 5px; + padding-left: 20px; + font-weight: 500; + height: 28px; + display: inline-block; + padding-top: 5px; } .btnmainslide { - height: 14px; - margin-top: 14px; - cursor: pointer; + height: 14px; + margin-top: 14px; + cursor: pointer; } .well { - padding: 19px 0px; - padding-top: 0px; - margin-bottom: 20px; - border: 0px; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0); - background: white; + padding: 19px 0px; + padding-top: 0px; + margin-bottom: 20px; + border: 0px; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0); + background: white; } .splitter { - z-index: 1; - border-left: 5px solid white; - width: 8px; - border-right: 1px solid #cccccc; - float: left; - height: 100%; - position: absolute; - margin-left: 240px; - padding: 0px; - background-color: white; + z-index: 1; + border-left: 5px solid white; + width: 8px; + border-right: 1px solid #cccccc; + float: left; + height: 100%; + position: absolute; + margin-left: 240px; + padding: 0px; + background-color: white; } .testClass { - padding-left: 30px; + padding-left: 30px; } .level { - padding-left: 16px; - padding-top: 0px; + padding-left: 16px; + padding-top: 0px; } .imgiconwidth { - margin-right: 5px; + margin-right: 5px; } .id { - padding-left: 8px; - color: #000; - font-weight: bold; - margin-left: 6px; + padding-left: 8px; + color: #000; + font-weight: bold; + margin-left: 6px; } .documentsGridHeaderContainer { - padding-left: 5px; - padding-right: 15px; - width: 200px; + padding-left: 5px; + padding-right: 15px; + width: 200px; } .documentsGridHeader { - padding: 8px; - color: #000; - font-weight: bold; + padding: 8px; + color: #000; + font-weight: bold; } .fixedWidthHeader { - width: 82px; + width: 82px; } .tabdocuments { - padding: 4px 4px -1px 0px; + padding: 4px 4px -1px 0px; } -#divcontent>.mongoDocumentEditor .monaco-editor.vs .redsquiggly { - display: none !important; +#divcontent > .mongoDocumentEditor .monaco-editor.vs .redsquiggly { + display: none !important; } td a { - color: #393939; + color: #393939; } td a:hover { - color: #393939; + color: #393939; } .loadMore { - padding-left: 32%; - cursor: pointer; + padding-left: 32%; + cursor: pointer; } .table-fixed thead { - width: 97%; - padding-left: 18px; + width: 97%; + padding-left: 18px; } .table-fixed tbody { - height: 510px; - overflow-y: auto; - width: 100%; - overflow-x: hidden; + height: 510px; + overflow-y: auto; + width: 100%; + overflow-x: hidden; } .table-fixed thead, @@ -929,383 +925,383 @@ td a:hover { .table-fixed tr, .table-fixed td, .table-fixed th { - display: block; + display: block; } .table-fixed tbody td, -.table-fixed thead>tr>th { - float: left; - border-bottom-width: 0; +.table-fixed thead > tr > th { + float: left; + border-bottom-width: 0; } a:hover, a:visited, a:active, a:link { - text-decoration: none; + text-decoration: none; } .tabs { - position: relative; - clear: both; - margin: 15px 0 25px 0; - display: table; - width: 100%; + position: relative; + clear: both; + margin: 15px 0 25px 0; + display: table; + width: 100%; } .tab { - float: left; + float: left; } .tab label { - padding: 10px; - border: 1px solid #bbbbbb; - margin-left: -1px; - position: inherit; - left: 1px; - color: #393939; + padding: 10px; + border: 1px solid #bbbbbb; + margin-left: -1px; + position: inherit; + left: 1px; + color: #393939; } -.tab [type=radio] { - display: none; +.tab [type="radio"] { + display: none; } .tabcontent { - position: absolute; - top: 30px; - left: 0; - right: 0; - bottom: 0; - padding: 15px 0px 20px 0; + position: absolute; + top: 30px; + left: 0; + right: 0; + bottom: 0; + padding: 15px 0px 20px 0; } -.tab [type=radio]:checked~label { - border: 1px solid #0072c6; - background-color: @AccentMediumHigh; - color: white; - z-index: 2; +.tab [type="radio"]:checked ~ label { + border: 1px solid #0072c6; + background-color: @AccentMediumHigh; + color: white; + z-index: 2; } -.tab [type=radio]:checked~label:hover { - border: 1px solid @AccentMediumHigh; - background-color: @AccentMediumHigh; - color: white; - z-index: 2; +.tab [type="radio"]:checked ~ label:hover { + border: 1px solid @AccentMediumHigh; + background-color: @AccentMediumHigh; + color: white; + z-index: 2; } -.tab [type=radio]:checked~label:active { - border: 1px solid #0072c6; - background-color: #0072c6; - color: white; - z-index: 2; +.tab [type="radio"]:checked ~ label:active { + border: 1px solid #0072c6; + background-color: #0072c6; + color: white; + z-index: 2; } -.tab [type=radio]:checked~label~.tabcontent { - z-index: 1; - display: initial; +.tab [type="radio"]:checked ~ label ~ .tabcontent { + z-index: 1; + display: initial; } -.tab [type=radio]:not(:checked)~label:hover { - border: 1px solid #969696; - background-color: #969696; - color: white; - cursor: pointer; +.tab [type="radio"]:not(:checked) ~ label:hover { + border: 1px solid #969696; + background-color: #969696; + color: white; + cursor: pointer; } -.tab [type=radio]:not(:checked)~label~.tabcontent { - display: none; +.tab [type="radio"]:not(:checked) ~ label ~ .tabcontent { + display: none; } ::-webkit-input-placeholder { - color: #969696; + color: #969696; } ::-moz-placeholder { - color: #969696; + color: #969696; } :-ms-input-placeholder { - color: #969696; + color: #969696; } :-moz-placeholder { - color: #969696; + color: #969696; } ::-ms-expand { - color: #969696; + color: #969696; } .form-errors { - color: white; - padding-left: 12px; + color: white; + padding-left: 12px; } .atagdetails { - padding-left: 55px!important; + padding-left: 55px !important; } .path { - color: lightgray; - font-style: italic; - padding-top: 12px; - padding-left: 20px; + color: lightgray; + font-style: italic; + padding-top: 12px; + padding-left: 20px; } .queryPath { - line-height: 16px; - padding-left: 33px; - padding-bottom: 12px; + line-height: 16px; + padding-left: 33px; + padding-bottom: 12px; } .filterDocCollapsed { - padding-left: 20px; + padding-left: 20px; } .filterDocCollapsed.active { - display: block; + display: block; } .filterDocExpanded { - padding-left: 20px; + padding-left: 20px; } .filterDocExpanded.active { - display: block; + display: block; } .filterbuttonpad { - padding-top: 10px; + padding-top: 10px; } .filterbtnstyle { - background: @AccentMediumHigh; - width: 90px; - height: 25px; - color: white; - border: none; - margin-left: 16px !important; + background: @AccentMediumHigh; + width: 90px; + height: 25px; + color: white; + border: none; + margin-left: 16px !important; } .filterbtnstyle:hover { - background: @AccentMediumHigh; - width: 90px; - height: 25px; - color: white; - border: none; - margin-left: 16px; + background: @AccentMediumHigh; + width: 90px; + height: 25px; + color: white; + border: none; + margin-left: 16px; } .filterbtnstyle:active { - background: #0072c6; - width: 90px; - height: 25px; - color: white; - border: none; - margin-left: 16px; + background: #0072c6; + width: 90px; + height: 25px; + color: white; + border: none; + margin-left: 16px; } .filterbtnstyle:focus { - background: #0072c6; - width: 90px; - height: 25px; - color: white; - border: none; - margin-left: 16px; - border: 1px solid #0072c6; + background: #0072c6; + width: 90px; + height: 25px; + color: white; + border: none; + margin-left: 16px; + border: 1px solid #0072c6; } .filterbtnstyle:not(:enabled) { - background: lightgray; - width: 90px; - height: 25px; - color: white; - border: none; + background: lightgray; + width: 90px; + height: 25px; + color: white; + border: none; } .hrline1 { - color: #d6d7d8; - margin-left: -20px; + color: #d6d7d8; + margin-left: -20px; } .filtdocheader { - font-size: 18px; + font-size: 18px; } .editFilter { - margin-left: 20px; + margin-left: 20px; } .filterdivs { - padding-top: 24px; - height: 45px; - margin-bottom: 20px; + padding-top: 24px; + height: 45px; + margin-bottom: 20px; } .filterclose { - padding: 0 10px; - cursor: pointer; + padding: 0 10px; + cursor: pointer; } .queryResultpreviousImg { - height: 14px; - width: 14px; - margin-right: 2px; + height: 14px; + width: 14px; + margin-right: 2px; } .queryResultnextImg { - height: 14px; - width: 14px; - margin-left: 2px; + height: 14px; + width: 14px; + margin-left: 2px; } .rowoverride { - margin-left: 7px; - margin-top: 20px; + margin-left: 7px; + margin-top: 20px; } .tab-content-override { - padding-left: 5px; - padding-top: 20px; + padding-left: 5px; + padding-top: 20px; } .paddingspan4 { - padding-top: 20px; - color: white; - padding-left: 25px; - padding-right: 25px; + padding-top: 20px; + color: white; + padding-left: 25px; + padding-right: 25px; } .colResizePointer { - cursor: col-resize; + cursor: col-resize; } -.nav-tabs>li>a { - border-radius: 2px 2px 0 0; - padding: 8px 0px 4px 0px; - color: #393939; - width: 130px; - text-align: center; - margin-right: 0px; - position: relative; +.nav-tabs > li > a { + border-radius: 2px 2px 0 0; + padding: 8px 0px 4px 0px; + color: #393939; + width: 130px; + text-align: center; + margin-right: 0px; + position: relative; } -.nav-tabs>li.active>a, -.nav-tabs>li.active>a:focus, -.nav-tabs>li.active>a:hover { - border-bottom-color: #FFF; +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:focus, +.nav-tabs > li.active > a:hover { + border-bottom-color: #fff; } .tabList { - float: left; - margin-bottom: -15px !important; + float: left; + margin-bottom: -15px !important; } .tab_Content { - width: 130px; - border-right: 1px solid #e0e0e0; - padding: 0px 22px 0px 17px; - margin-left: -1px; + width: 130px; + border-right: 1px solid #e0e0e0; + padding: 0px 22px 0px 17px; + margin-left: -1px; } .tab_Content:hover { - width: 130px; - border-right: 1px solid #e0e0e0; - padding: 0px 22px 0px 17px; - margin-left: -1px; + width: 130px; + border-right: 1px solid #e0e0e0; + padding: 0px 22px 0px 17px; + margin-left: -1px; } .tab_Content:active { - width: 130px; - border-right: 1px; - padding: 0px 22px 0px 17px; - margin-left: -1px; + width: 130px; + border-right: 1px; + padding: 0px 22px 0px 17px; + margin-left: -1px; } .tabtext-center { - max-width: 110px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding-left: 2px; + max-width: 110px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-left: 2px; } .tabIconSection { - width: 30px; - float: right; - top: -16px; - position: relative; - padding: 2px 12px 0 13px; + width: 30px; + float: right; + top: -16px; + position: relative; + padding: 2px 12px 0 13px; } -.nav-tabs>li>a:active { - background-color: #e0e0e0; - border-color: @AccentMediumHigh; +.nav-tabs > li > a:active { + background-color: #e0e0e0; + border-color: @AccentMediumHigh; } -.nav-tabs>li>a:active .tab_Content { - border: transparent; - width: 130px; +.nav-tabs > li > a:active .tab_Content { + border: transparent; + width: 130px; } .close-Icon { - background-image: url(../images/close-black.svg); - background-repeat: no-repeat; - padding: 0px 0px 0px 11px; + background-image: url(../images/close-black.svg); + background-repeat: no-repeat; + padding: 0px 0px 0px 11px; } .close-Icon:hover { - background-image: url(../images/close-black-hover.svg); - background-repeat: no-repeat; - padding: 0px 0px 0px 11px; + background-image: url(../images/close-black-hover.svg); + background-repeat: no-repeat; + padding: 0px 0px 0px 11px; } .clickableLink { - color: @AccentMediumHigh; - font-family: 'Segoe UI'; - font-size: 12px; - cursor: pointer; + color: @AccentMediumHigh; + font-family: "Segoe UI"; + font-size: 12px; + cursor: pointer; } .clickableLink:hover { - background-color: #e7f6fc; + background-color: #e7f6fc; } .clickableLink:active { - background-color: #e6f8fe; + background-color: #e6f8fe; } .clickableLink:focus { - outline: 1px dashed #000000; - outline-offset: 0px; + outline: 1px dashed #000000; + outline-offset: 0px; } .paneselect { - height: 23px; + height: 23px; } .headerWithoutPartitionKey { - width: 172px; + width: 172px; } .headerWithPartitionKey { - width: 86px; + width: 86px; } -input.codeblock{ - background-color: @BaseMediumLow; - color: #252525; - border: 1px solid @BaseMediumHigh; - box-sizing: border-box; - font-size: @mediumFontSize; - height: 23px; - outline: 0; - padding: 2px 8px 4px; - width: 60%; - min-width: 960px; - cursor: text; +input.codeblock { + background-color: @BaseMediumLow; + color: #252525; + border: 1px solid @BaseMediumHigh; + box-sizing: border-box; + font-size: @mediumFontSize; + height: 23px; + outline: 0; + padding: 2px 8px 4px; + width: 60%; + min-width: 960px; + cursor: text; } -#divQuickStartConnections{ - padding-bottom: 10px; -} \ No newline at end of file +#divQuickStartConnections { + padding-bottom: 10px; +} diff --git a/src/quickstart.html b/src/quickstart.html index fc89be86f..b082231a8 100644 --- a/src/quickstart.html +++ b/src/quickstart.html @@ -64,41 +64,40 @@
  • - Spring Boot Springboot + Spring Boot Spring Boot
  • -
    + +
    -
    +
    1
    - Open and run a sample .NET app + Create a new .NET app

    - We created a sample .NET app connected to your Azure Cosmos DB Emulator instance. Download, extract, - build and run the app. + Follow this + tutorial + + to create a new .NET app connected to Azure Cosmos DB.

    -
    -
    +
    1
    - Open and run a sample Java app + Create a new Java app

    - We created a sample Java app connected to your Azure Cosmos DB Emulator instance. Download, extract, - build and run the app. -

    - -

    - Follow instructions in the readme.md to setup prerequisites needed to run Java web apps, if you - haven’t already. + Follow this + tutorial + + to create a new Java app connected to Azure Cosmos DB.

    @@ -106,20 +105,18 @@
    -
    +
    1
    - Open and run a sample Node.js app + Create a new Node.js app

    - We created a sample Node.js app connected to your Azure Cosmos DB Emulator instance. Download, - extract, build and run the app. -

    - -

    - Run npm install and npm start, and navigate to - http://localhost:3000. + Follow this + tutorial + + to create a new Node.js app connected to Azure Cosmos DB.

    @@ -127,7 +124,7 @@
    -
    +
    1
    Create a new Python app @@ -141,9 +138,8 @@
    -
    -
    +
    1
    Create a new Go app @@ -158,7 +154,7 @@
    -
    +
    1
    Create a new Spring Boot app From e90e1fc581a34e866e99c013515de78b114b67b2 Mon Sep 17 00:00:00 2001 From: sakshigupta12feb Date: Wed, 30 Apr 2025 17:48:15 +0530 Subject: [PATCH 23/87] Updated the Migrate data link (#2122) * updated the Migrate data link * updated the Migrate data link (removed en-us) --------- Co-authored-by: Sakshi Gupta --- src/Explorer/SplashScreen/SplashScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index 495bc03ee..2c7f778a4 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -817,7 +817,7 @@ export class SplashScreen extends React.Component { private vcoreMongoNextStepItems: { link: string; title: string; description: string }[] = [ { - link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/how-to-migrate-native-tools?tabs=export-import", + link: "https://learn.microsoft.com/azure/cosmos-db/mongodb/vcore/migration-options", title: "Migrate Data", description: "", }, From fe73d0a1c63ba92e8c3e76dfcdcd0af60fa4e4df Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Wed, 30 Apr 2025 17:37:54 +0200 Subject: [PATCH 24/87] Fix Fabric Native ReadOnly mode (#2123) * Add FabricNativeReadOnly mode * Hide Settings for Fabric native readonly * Fix strict compil --- src/Explorer/Sidebar.tsx | 3 ++- src/Explorer/SplashScreen/FabricHome.tsx | 19 ++++++++++++++++--- src/Explorer/Tree/treeNodeUtil.tsx | 4 ++-- src/Platform/Fabric/FabricUtil.ts | 1 + 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Explorer/Sidebar.tsx b/src/Explorer/Sidebar.tsx index f3db58d8a..ce111dab3 100644 --- a/src/Explorer/Sidebar.tsx +++ b/src/Explorer/Sidebar.tsx @@ -27,7 +27,7 @@ import { CosmosFluentProvider, cosmosShorthands, tokens } from "Explorer/Theme/T import { ResourceTree } from "Explorer/Tree/ResourceTree"; import { useDatabases } from "Explorer/useDatabases"; import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboardActionGroup } from "KeyboardShortcuts"; -import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil"; +import { isFabric, isFabricMirrored, isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil"; import { userContext } from "UserContext"; import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils"; import { Allotment, AllotmentHandle } from "allotment"; @@ -318,6 +318,7 @@ export const SidebarContainer: React.FC = ({ explorer }) => { const hasGlobalCommands = !( isFabricMirrored() || + isFabricNativeReadOnly() || userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo" ); diff --git a/src/Explorer/SplashScreen/FabricHome.tsx b/src/Explorer/SplashScreen/FabricHome.tsx index c235604d4..7db6ee041 100644 --- a/src/Explorer/SplashScreen/FabricHome.tsx +++ b/src/Explorer/SplashScreen/FabricHome.tsx @@ -5,7 +5,7 @@ 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 } from "Platform/Fabric/FabricUtil"; +import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil"; import * as React from "react"; import { userContext } from "UserContext"; import CosmosDbBlackIcon from "../../../images/CosmosDB_black.svg"; @@ -62,6 +62,15 @@ const useStyles = makeStyles({ margin: "auto", }, }, + single: { + gridColumn: "1 / 4", + gridRow: "1 / 3", + "& svg": { + width: "64px", + height: "64px", + margin: "auto", + }, + }, buttonContainer: { height: "100%", display: "flex", @@ -150,7 +159,11 @@ export const FabricHomeScreen: React.FC = (props: SplashScree }, ]; - return ( + return isFabricNativeReadOnly() ? ( +
    + +
    + ) : (
    @@ -159,7 +172,7 @@ export const FabricHomeScreen: React.FC = (props: SplashScree ); }; - const title = "Build your database"; + const title = isFabricNativeReadOnly() ? "Use your database" : "Build your database"; return ( <> diff --git a/src/Explorer/Tree/treeNodeUtil.tsx b/src/Explorer/Tree/treeNodeUtil.tsx index 838d5c1f0..04eafed3f 100644 --- a/src/Explorer/Tree/treeNodeUtil.tsx +++ b/src/Explorer/Tree/treeNodeUtil.tsx @@ -6,7 +6,7 @@ import StoredProcedure from "Explorer/Tree/StoredProcedure"; import Trigger from "Explorer/Tree/Trigger"; import UserDefinedFunction from "Explorer/Tree/UserDefinedFunction"; import { useDatabases } from "Explorer/useDatabases"; -import { isFabric, isFabricMirrored, isFabricNative } from "Platform/Fabric/FabricUtil"; +import { isFabric, isFabricMirrored, isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil"; import { getItemName } from "Utils/APITypeUtils"; import { isServerlessAccount } from "Utils/CapabilityUtils"; import { useTabs } from "hooks/useTabs"; @@ -292,7 +292,7 @@ const buildCollectionNodeChildren = ( contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(container, collection), }); - if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) { + if ((userContext.apiType !== "Cassandra" || !isServerlessAccount()) && !isFabricNativeReadOnly()) { let id = ""; if (collection.isSampleCollection) { id = database.isDatabaseShared() ? "sampleSettings" : "sampleScaleSettings"; diff --git a/src/Platform/Fabric/FabricUtil.ts b/src/Platform/Fabric/FabricUtil.ts index a9a653dd7..c2332e4bc 100644 --- a/src/Platform/Fabric/FabricUtil.ts +++ b/src/Platform/Fabric/FabricUtil.ts @@ -136,3 +136,4 @@ export const isFabricMirroredAAD = (): boolean => export const isFabricMirrored = (): boolean => isFabricMirroredKey() || isFabricMirroredAAD(); export const isFabricNative = (): boolean => isFabric() && userContext.fabricContext?.artifactType === CosmosDbArtifactType.NATIVE; +export const isFabricNativeReadOnly = (): boolean => isFabricNative() && !!userContext.fabricContext?.isReadOnly; From bb66deb3a46cdb07677a3fb9d0057c71f5ed7e57 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Wed, 30 Apr 2025 15:18:11 -0500 Subject: [PATCH 25/87] Added more test cases and fix system partition key load issue (#2126) * Added more test cases and fix system partition key load issue * Fix unit tests and fix ci * Updated test snapsho --- .github/workflows/ci.yml | 18 ++++---- playwright.config.ts | 41 ++++++++++++++++--- .../Controls/InputDataList/InputDataList.tsx | 1 + .../Tabs/DocumentsTabV2/DocumentsTabV2.tsx | 9 +++- .../DocumentsTabV2.test.tsx.snap | 1 + test/CORSBypass.ts | 23 +++++++++++ test/fx.ts | 16 ++++++-- test/mongo/document.spec.ts | 11 ++++- test/sql/document.spec.ts | 17 ++++---- test/sql/testCases.ts | 19 ++++++++- test/testData.ts | 11 +++-- 11 files changed, 136 insertions(+), 31 deletions(-) create mode 100644 test/CORSBypass.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56ed46edc..228470f72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -164,24 +164,24 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] - shardTotal: [8] + shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + shardTotal: [16] steps: - uses: actions/checkout@v4 - - name: "Az CLI login" - uses: azure/login@v1 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Use Node.js 18.x uses: actions/setup-node@v4 with: node-version: 18.x - run: npm ci - run: npx playwright install --with-deps + - name: "Az CLI login" + uses: Azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}} - run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3 - name: Upload blob report to GitHub Actions Artifacts if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 diff --git a/playwright.config.ts b/playwright.config.ts index 80ba367bf..b1f6a622d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -37,20 +37,51 @@ export default defineConfig({ }, { name: "firefox", - use: { ...devices["Desktop Firefox"] }, + use: { + ...devices["Desktop Firefox"], + launchOptions: { + firefoxUserPrefs: { + "security.fileuri.strict_origin_policy": false, + "network.http.referer.XOriginPolicy": 0, + "network.http.referer.trimmingPolicy": 0, + "privacy.file_unique_origin": false, + "security.csp.enable": false, + "network.cors_preflight.allow_client_cert": true, + "dom.security.https_first": false, + "network.http.cross-origin-embedder-policy": false, + "network.http.cross-origin-opener-policy": false, + "browser.tabs.remote.useCrossOriginPolicy": false, + "browser.tabs.remote.useCORP": false, + }, + args: ["--disable-web-security"], + }, + }, }, { name: "webkit", - use: { ...devices["Desktop Safari"] }, + use: { + ...devices["Desktop Safari"], + }, }, - /* Test against branded browsers. */ { name: "Google Chrome", - use: { ...devices["Desktop Chrome"], channel: "chrome" }, // or 'chrome-beta' + use: { + ...devices["Desktop Chrome"], + channel: "chrome", + launchOptions: { + args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"], + }, + }, }, { name: "Microsoft Edge", - use: { ...devices["Desktop Edge"], channel: "msedge" }, // or 'msedge-dev' + use: { + ...devices["Desktop Edge"], + channel: "msedge", + launchOptions: { + args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"], + }, + }, }, ], diff --git a/src/Explorer/Controls/InputDataList/InputDataList.tsx b/src/Explorer/Controls/InputDataList/InputDataList.tsx index cd31db53b..2f483d362 100644 --- a/src/Explorer/Controls/InputDataList/InputDataList.tsx +++ b/src/Explorer/Controls/InputDataList/InputDataList.tsx @@ -193,6 +193,7 @@ export const InputDataList: FC = ({ <> (partitionKey?.systemKey ? [] : _collection?.partitionKeyPropertyHeaders || partitionKey?.paths), - [_collection?.partitionKeyPropertyHeaders, partitionKey?.paths, partitionKey?.systemKey], + () => + isPreferredApiMongoDB && partitionKey?.systemKey + ? [] + : _collection?.partitionKeyPropertyHeaders || partitionKey?.paths, + [_collection?.partitionKeyPropertyHeaders, partitionKey?.paths, partitionKey?.systemKey, isPreferredApiMongoDB], ); let partitionKeyProperties = useMemo(() => { return partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) => @@ -2116,6 +2119,7 @@ export const DocumentsTabComponent: React.FunctionComponent + +
    + )} +
    + ); + }; + return ( <>
    @@ -67,6 +234,7 @@ const ResultsTab: React.FC = ({ queryResults, isMongoDB, execu aria-label="Copy" onClick={onClickCopyResults} /> +
    From 2d7631c358827b6fb2cc7b3885697281ac2a0bf1 Mon Sep 17 00:00:00 2001 From: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com> Date: Tue, 27 May 2025 11:54:20 -0500 Subject: [PATCH 52/87] Added throughput buckets in the scale update (#2164) --- src/Explorer/Controls/Settings/SettingsComponent.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 29a85845c..6d8c875f8 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -1091,6 +1091,7 @@ export class SettingsComponent extends React.Component Date: Wed, 28 May 2025 12:25:14 -0700 Subject: [PATCH 53/87] Add login.microsoftonline.com to CSP. (#2167) --- web.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web.config b/web.config index f29c8b878..26e235589 100644 --- a/web.config +++ b/web.config @@ -30,7 +30,7 @@ - + From c5ed5371095c84bdf186781a91b0373e018a1827 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Thu, 29 May 2025 07:27:15 -0700 Subject: [PATCH 54/87] Fix invalid reference in frame-src and add delimiter for (#2168) frame-ancestors. --- web.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web.config b/web.config index 26e235589..294afb8d8 100644 --- a/web.config +++ b/web.config @@ -30,7 +30,7 @@ - + From 0fc6647627fd6408a122894b98e78482398ecd1b Mon Sep 17 00:00:00 2001 From: asier-isayas Date: Thu, 29 May 2025 11:35:06 -0400 Subject: [PATCH 55/87] Upgrade Cosmos SDK to v4.3 (#2137) * Upgrade Cosmos SDK to v4.3 * push pkg lock * fix tests * fix package-lock.json * fix package-lock * fix package-lock.json * fix package-lock.json * log console warning when RU limit is reached * fix tests * fix format * fix tests * added description to RU limit message * show warning icon on tab header --------- Co-authored-by: Asier Isayas --- package-lock.json | 197 ++++++++++++++---- package.json | 2 +- preview/package-lock.json | 6 +- src/Common/IteratorUtilities.ts | 11 +- src/Common/QueryError.ts | 16 +- .../__snapshots__/queryDocuments.test.ts.snap | 2 + src/Common/dataAccess/queryDocuments.ts | 1 + src/Common/dataAccess/queryDocumentsPage.ts | 4 +- src/Contracts/ViewModels.ts | 1 + .../GraphExplorerComponent/GraphExplorer.tsx | 10 +- .../Menus/NotificationConsole/ConsoleData.tsx | 1 + .../NotificationConsoleComponent.tsx | 9 + ...NotificationConsoleComponent.test.tsx.snap | 26 +++ .../Panes/SettingsPane/SettingsPane.tsx | 41 +++- .../__snapshots__/SettingsPane.test.tsx.snap | 45 ++++ .../Tabs/QueryTab/QueryTabComponent.tsx | 24 +-- src/Explorer/Tabs/Tabs.tsx | 18 ++ src/Explorer/Tabs/TabsBase.ts | 1 + src/Shared/StorageUtility.ts | 1 + src/Utils/NotificationConsoleUtils.ts | 4 + src/Utils/QueryUtils.ts | 15 ++ webpack.config.js | 9 +- 22 files changed, 354 insertions(+), 90 deletions(-) diff --git a/package-lock.json b/package-lock.json index e18653826..bef67e871 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.2.0-beta.1", + "@azure/cosmos": "4.3.0", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "4.5.0", "@azure/msal-browser": "2.14.2", @@ -290,57 +290,69 @@ "version": "2.6.2", "license": "0BSD" }, - "node_modules/@azure/core-rest-pipeline": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.0.tgz", - "integrity": "sha512-QSoGUp4Eq/gohEFNJaUOwTN7BCc2nHTjjbm75JT0aD7W65PWM1H/tItz0GsABn22uaKyGxiMhWQLt2r+FGU89Q==", + "node_modules/@azure/core-http-compat": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.0.tgz", + "integrity": "sha512-qLQujmUypBBG0gxHd0j6/Jdmul6ttl24c8WGiLXIk7IHXdBlfoBqW27hyz3Xn6xbfdyVSarl1Ttbk0AwnZBYCw==", "dependencies": { "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.8.0", - "@azure/core-tracing": "^1.0.1", - "@azure/core-util": "^1.11.0", + "@azure/core-client": "^1.3.0", + "@azure/core-rest-pipeline": "^1.20.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@azure/core-rest-pipeline/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "node_modules/@azure/core-lro/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", "dependencies": { - "debug": "^4.3.4" + "tslib": "^2.6.2" }, "engines": { - "node": ">= 14" + "node": ">=18.0.0" } }, - "node_modules/@azure/core-rest-pipeline/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } + "node_modules/@azure/core-paging/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, - "node_modules/@azure/core-rest-pipeline/node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "node_modules/@azure/core-rest-pipeline": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.20.0.tgz", + "integrity": "sha512-ASoP8uqZBS3H/8N8at/XwFr6vYrRP3syTK0EUjDXQy0Y1/AUS+QeIRThKmTNJO2RggvBBxaXDPM7YoIwDGeA0g==", "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.8.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@typespec/ts-http-runtime": "^0.2.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">= 14" + "node": ">=18.0.0" } }, "node_modules/@azure/core-rest-pipeline/node_modules/tslib": { @@ -379,15 +391,16 @@ "license": "0BSD" }, "node_modules/@azure/cosmos": { - "version": "4.2.0-beta.1", - "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.2.0-beta.1.tgz", - "integrity": "sha512-mREONehm1DxjEKXGaNU6Wmpf9Ckb9IrhKFXhDFVs45pxmoEb3y2s/Ub0owuFmqlphpcS1zgtYQn5exn+lwnJuQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.3.0.tgz", + "integrity": "sha512-0Ls3l1uWBBSphx6YRhnM+w7rSvq8qVugBCdO6kSiNuRYXEf6+YWLjbzz4e7L2kkz/6ScFdZIOJYP+XtkiRYOhA==", "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.7.1", "@azure/core-rest-pipeline": "^1.15.1", "@azure/core-tracing": "^1.1.1", "@azure/core-util": "^1.8.1", + "@azure/keyvault-keys": "^4.8.0", "fast-json-stable-stringify": "^2.1.0", "jsbi": "^4.3.0", "priorityqueuejs": "^2.0.0", @@ -492,14 +505,66 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, - "node_modules/@azure/logger": { - "version": "1.0.4", - "license": "MIT", + "node_modules/@azure/keyvault-common": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz", + "integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==", "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", "tslib": "^2.2.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/@azure/keyvault-common/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.9.0.tgz", + "integrity": "sha512-ZBP07+K4Pj3kS4TF4XdkqFcspWwBHry3vJSOFM5k5ZABvf7JfiMonvaFk2nBF6xjlEbMpz5PE1g45iTMme0raQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-http-compat": "^2.0.1", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.8.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.0.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/keyvault-keys/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/@azure/logger": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.2.0.tgz", + "integrity": "sha512-0hKEzLhpw+ZTAfNJyRrn6s+V0nDWzXk9OjBr2TiGIu0OfMr5s2V4FpKLTAK3Ca5r5OKLbf4hkOGDPyiRjie/jA==", + "dependencies": { + "@typespec/ts-http-runtime": "^0.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@azure/logger/node_modules/tslib": { @@ -13074,6 +13139,56 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.2.2.tgz", + "integrity": "sha512-Gz/Sm64+Sq/vklJu1tt9t+4R2lvnud8NbTD/ZfpZtMiUX7YeVpCA8j6NSW8ptwcoLL+NmYANwqP8DV0q/bwl2w==", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@typespec/ts-http-runtime/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@typespec/ts-http-runtime/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@typespec/ts-http-runtime/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@typespec/ts-http-runtime/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/@ungap/url-search-params": { "version": "0.2.2", "license": "ISC" diff --git a/package.json b/package.json index b5914047b..faf4cdac9 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.2.0-beta.1", + "@azure/cosmos": "4.3.0", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "4.5.0", "@azure/msal-browser": "2.14.2", diff --git a/preview/package-lock.json b/preview/package-lock.json index daf213a84..ab6c6ecbb 100644 --- a/preview/package-lock.json +++ b/preview/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "dependencies": { "@azure/arm-cosmosdb": "9.1.0", - "@azure/cosmos": "4.2.0-beta.1", + "@azure/cosmos": "4.3.0", "@azure/cosmos-language-service": "0.0.5", "@azure/identity": "4.5.0", "@azure/msal-browser": "2.14.2", @@ -377,8 +377,8 @@ "license": "0BSD" }, "node_modules/@azure/cosmos": { - "version": "4.2.0-beta.1", - "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.2.0-beta.1.tgz", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.3.0.tgz", "integrity": "sha512-mREONehm1DxjEKXGaNU6Wmpf9Ckb9IrhKFXhDFVs45pxmoEb3y2s/Ub0owuFmqlphpcS1zgtYQn5exn+lwnJuQ==", "dependencies": { "@azure/abort-controller": "^2.0.0", diff --git a/src/Common/IteratorUtilities.ts b/src/Common/IteratorUtilities.ts index 0cbe8ea63..827033fd7 100644 --- a/src/Common/IteratorUtilities.ts +++ b/src/Common/IteratorUtilities.ts @@ -1,4 +1,3 @@ -import { QueryOperationOptions } from "@azure/cosmos"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import * as Constants from "../Common/Constants"; import { QueryResults } from "../Contracts/ViewModels"; @@ -14,18 +13,14 @@ interface QueryResponse { } export interface MinimalQueryIterator { - fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise; + fetchNext: () => Promise; } // Pick, "fetchNext">; -export function nextPage( - documentsIterator: MinimalQueryIterator, - firstItemIndex: number, - queryOperationOptions?: QueryOperationOptions, -): Promise { +export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise { TelemetryProcessor.traceStart(Action.ExecuteQuery); - return documentsIterator.fetchNext(queryOperationOptions).then((response) => { + return documentsIterator.fetchNext().then((response) => { TelemetryProcessor.traceSuccess(Action.ExecuteQuery, { dataExplorerArea: Constants.Areas.Tab }); const documents = response.resources; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/Common/QueryError.ts b/src/Common/QueryError.ts index c1bab1c02..de84deee7 100644 --- a/src/Common/QueryError.ts +++ b/src/Common/QueryError.ts @@ -1,5 +1,4 @@ import { monaco } from "Explorer/LazyMonaco"; -import { getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility"; export enum QueryErrorSeverity { Error = "Error", @@ -103,20 +102,9 @@ export interface ErrorEnrichment { learnMoreUrl?: string; } -const REPLACEMENT_MESSAGES: Record string> = { - OPERATION_RU_LIMIT_EXCEEDED: (original) => { - if (ruThresholdEnabled()) { - const threshold = getRUThreshold(); - return `Query exceeded the Request Unit (RU) limit of ${threshold} RUs. You can change this limit in Data Explorer settings.`; - } - return original; - }, -}; +const REPLACEMENT_MESSAGES: Record string> = {}; -const HELP_LINKS: Record = { - OPERATION_RU_LIMIT_EXCEEDED: - "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer#configure-request-unit-threshold", -}; +const HELP_LINKS: Record = {}; export default class QueryError { message: string; diff --git a/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap b/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap index bcf0034a4..0c7f67c57 100644 --- a/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap +++ b/src/Common/dataAccess/__snapshots__/queryDocuments.test.ts.snap @@ -3,6 +3,7 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] = ` { "disableNonStreamingOrderByQuery": true, + "enableQueryControl": false, "enableScanInQuery": true, "forceQueryPlan": true, "maxDegreeOfParallelism": 0, @@ -14,6 +15,7 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] = exports[`getCommonQueryOptions reads from localStorage 1`] = ` { "disableNonStreamingOrderByQuery": true, + "enableQueryControl": false, "enableScanInQuery": true, "forceQueryPlan": true, "maxDegreeOfParallelism": 17, diff --git a/src/Common/dataAccess/queryDocuments.ts b/src/Common/dataAccess/queryDocuments.ts index 223fe987d..44cd6f4e9 100644 --- a/src/Common/dataAccess/queryDocuments.ts +++ b/src/Common/dataAccess/queryDocuments.ts @@ -26,6 +26,7 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => { options.maxItemCount || (storedItemPerPageSetting !== undefined && storedItemPerPageSetting) || Queries.itemsPerPage; + options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled); options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism); options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled(); return options; diff --git a/src/Common/dataAccess/queryDocumentsPage.ts b/src/Common/dataAccess/queryDocumentsPage.ts index 17e84ba28..556ed290c 100644 --- a/src/Common/dataAccess/queryDocumentsPage.ts +++ b/src/Common/dataAccess/queryDocumentsPage.ts @@ -1,4 +1,3 @@ -import { QueryOperationOptions } from "@azure/cosmos"; import { QueryResults } from "../../Contracts/ViewModels"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { getEntityName } from "../DocumentUtility"; @@ -9,13 +8,12 @@ export const queryDocumentsPage = async ( resourceName: string, documentsIterator: MinimalQueryIterator, firstItemIndex: number, - queryOperationOptions?: QueryOperationOptions, ): Promise => { const entityName = getEntityName(); const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`); try { - const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions); + const result: QueryResults = await nextPage(documentsIterator, firstItemIndex); const itemCount = (result.documents && result.documents.length) || 0; logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`); return result; diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 403eb9e79..a49ab779b 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -50,6 +50,7 @@ export interface QueryResults extends QueryResultsMetadata { roundTrips?: number; headers?: any; queryMetrics?: QueryMetrics; + ruThresholdExceeded?: boolean; } export interface Button { diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx index 5ca296b57..0924a06b3 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx @@ -16,7 +16,12 @@ import * as StorageUtility from "../../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; -import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; +import { + logConsoleError, + logConsoleInfo, + logConsoleProgress, + logConsoleWarning, +} from "../../../Utils/NotificationConsoleUtils"; import { EditorReact } from "../../Controls/Editor/EditorReact"; import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent"; import * as TabComponent from "../../Controls/Tabs/TabComponent"; @@ -1083,6 +1088,7 @@ export class GraphExplorer extends React.Component void; public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void; public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void; + public static reportToConsole(type: ConsoleDataType.Warning, msg: string, ...errorData: any[]): void; public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) { let errorDataStr = ""; if (errorData && errorData.length > 0) { @@ -1099,6 +1105,8 @@ export class GraphExplorer extends React.Component data.type === ConsoleDataType.Info, ).length; + const numWarningItems = this.state.allConsoleData.filter( + (data: ConsoleData) => data.type === ConsoleDataType.Warning, + ).length; return (
    @@ -118,6 +122,10 @@ export class NotificationConsoleComponent extends React.Component< Info items {numInfoItems} + + Warning items + {numWarningItems} + {userContext.features.pr && } @@ -198,6 +206,7 @@ export class NotificationConsoleComponent extends React.Component< {item.type === ConsoleDataType.Info && info} {item.type === ConsoleDataType.Error && error} {item.type === ConsoleDataType.InProgress && in progress} + {item.type === ConsoleDataType.Warning && warning} {item.date} {item.message} diff --git a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap index b2672aeec..5d471f177 100644 --- a/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap +++ b/src/Explorer/Menus/NotificationConsole/__snapshots__/NotificationConsoleComponent.test.tsx.snap @@ -59,6 +59,19 @@ exports[`NotificationConsoleComponent renders the console 1`] = ` 0 + + Warning items + + 0 + + + + Warning items + + 0 + + = ({ ? LocalStorageUtility.getEntryNumber(StorageKey.MaxWaitTimeInSeconds) : Constants.Queries.DefaultMaxWaitTimeInSeconds, ); + const [queryControlEnabled, setQueryControlEnabled] = useState( + LocalStorageUtility.hasItem(StorageKey.QueryControlEnabled) + ? LocalStorageUtility.getEntryString(StorageKey.QueryControlEnabled) === "true" + : false, + ); const [maxDegreeOfParallelism, setMaxDegreeOfParallelism] = useState( LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism) ? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism) @@ -204,6 +209,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ !isEmulator; const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin" && !isEmulator; const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator; + const shouldShowEnhancedQueryControl = userContext.apiType === "SQL"; const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator; const showEnableEntraIdRbac = isDataplaneRbacSupported(userContext.apiType) && @@ -381,6 +387,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ LocalStorageUtility.setEntryNumber(StorageKey.MaxWaitTimeInSeconds, MaxWaitTimeInSeconds); LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString()); LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString()); + LocalStorageUtility.setEntryString(StorageKey.QueryControlEnabled, queryControlEnabled.toString()); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism); LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString()); LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString()); @@ -410,6 +417,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ `Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`, ); logConsoleInfo(`${crossPartitionQueryEnabled ? "Enabled" : "Disabled"} cross-partition query feed option`); + logConsoleInfo(`${queryControlEnabled ? "Enabled" : "Disabled"} query control option`); logConsoleInfo( `Updated the max degree of parallelism query feed option to ${LocalStorageUtility.getEntryNumber( StorageKey.MaxDegreeOfParellism, @@ -760,7 +768,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({ )} -
    RU Limit
    @@ -943,6 +950,38 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
    )} + {shouldShowEnhancedQueryControl && ( + + +
    Enhanced query control
    +
    + +
    + + setQueryControlEnabled(!queryControlEnabled)} + label="Enable query control" + /> +
    + + + )} {shouldShowParallelismOption && ( diff --git a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap index 41a75a38f..41759f1c7 100644 --- a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap +++ b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap @@ -495,6 +495,51 @@ exports[`Settings Pane should render Default properly 1`] = `
    + + +
    + Enhanced query control +
    +
    + +
    +
    + Query up to the max degree of parallelism. + + + Learn more + + +
    + +
    +
    +
    diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 747ff3349..c55923486 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-console */ -import { FeedOptions, QueryOperationOptions } from "@azure/cosmos"; +import { FeedOptions } from "@azure/cosmos"; import { AuthType } from "AuthType"; import QueryError, { createMonacoErrorLocationResolver, createMonacoMarkersForQueryErrors } from "Common/QueryError"; import { SplitterDirection } from "Common/Splitter"; @@ -19,7 +19,7 @@ import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil"; import { useSelectedNode } from "Explorer/useSelectedNode"; import { KeyboardAction } from "KeyboardShortcuts"; import { QueryConstants } from "Shared/Constants"; -import { LocalStorageUtility, StorageKey, getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility"; +import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Allotment } from "allotment"; import { useClientWriteEnabled } from "hooks/useClientWriteEnabled"; @@ -369,22 +369,9 @@ class QueryTabComponentImpl extends React.Component - await queryDocumentsPage( - this.props.collection && this.props.collection.id(), - this._iterator, - firstItemIndex, - queryOperationOptions, - ); + await queryDocumentsPage(this.props.collection && this.props.collection.id(), this._iterator, firstItemIndex); this.props.tabsBaseInstance.isExecuting(true); this.setState({ isExecuting: true, @@ -424,6 +411,9 @@ class QueryTabComponentImpl extends React.Component {useObservable(tab?.isExecutionError || ko.observable(false)) && } + {useObservable(tab?.isExecutionWarning || ko.observable(false)) && ( + + )} {isTabExecuting(tab, tabKind) && ( Loading )} @@ -194,6 +198,20 @@ const ErrorIcon = ({ tab, active }: { tab: Tab; active: boolean }) => (
    ); +const WarningIcon = ({ tab, active }: { tab: Tab; active: boolean }) => ( +
    tab.onErrorDetailsClick(undefined, e)} + onKeyPress={({ nativeEvent: e }) => tab.onErrorDetailsKeyPress(undefined, e)} + > + Warning Icon +
    +); + function TabPane({ tab, active }: { tab: Tab; active: boolean }) { const ref = useRef(); const attrs = { diff --git a/src/Explorer/Tabs/TabsBase.ts b/src/Explorer/Tabs/TabsBase.ts index 4d82d66a4..2b97fed3e 100644 --- a/src/Explorer/Tabs/TabsBase.ts +++ b/src/Explorer/Tabs/TabsBase.ts @@ -27,6 +27,7 @@ export default class TabsBase extends WaitsForTemplateViewModel { public tabTitle: ko.Observable; public tabPath: ko.Observable; public isExecutionError = ko.observable(false); + public isExecutionWarning = ko.observable(false); public isExecuting = ko.observable(false); protected _theme: string; public onLoadStartKey: number; diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts index f2baa85da..c5b2f18dd 100644 --- a/src/Shared/StorageUtility.ts +++ b/src/Shared/StorageUtility.ts @@ -21,6 +21,7 @@ export enum StorageKey { DatabaseAccountId, EncryptedKeyToken, IsCrossPartitionQueryEnabled, + QueryControlEnabled, MaxDegreeOfParellism, IsGraphAutoVizDisabled, TenantId, diff --git a/src/Utils/NotificationConsoleUtils.ts b/src/Utils/NotificationConsoleUtils.ts index b3c63e5a7..927c3c490 100644 --- a/src/Utils/NotificationConsoleUtils.ts +++ b/src/Utils/NotificationConsoleUtils.ts @@ -23,3 +23,7 @@ export const logConsoleError = (msg: string): void => { export const logConsoleInfo = (msg: string): void => { log(ConsoleDataType.Info, msg); }; + +export const logConsoleWarning = (msg: string): void => { + log(ConsoleDataType.Warning, msg); +}; diff --git a/src/Utils/QueryUtils.ts b/src/Utils/QueryUtils.ts index 643dd526b..08c7a73d0 100644 --- a/src/Utils/QueryUtils.ts +++ b/src/Utils/QueryUtils.ts @@ -1,4 +1,7 @@ import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos"; +import { getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility"; +import { userContext } from "UserContext"; +import { logConsoleWarning } from "Utils/NotificationConsoleUtils"; import * as DataModels from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; @@ -86,6 +89,18 @@ export const queryPagesUntilContentPresent = async ( results.roundTrips = roundTrips; results.requestCharge = Number(results.requestCharge) + netRequestCharge; netRequestCharge = Number(results.requestCharge); + + if (results.hasMoreResults && userContext.apiType === "SQL" && ruThresholdEnabled()) { + const ruThreshold: number = getRUThreshold(); + if (netRequestCharge > ruThreshold) { + logConsoleWarning( + `Warning: Query has exceeded the Request Unit threshold of ${ruThreshold} RUs. Query results show only those documents returned before the threshold was exceeded`, + ); + results.ruThresholdExceeded = true; + return results; + } + } + const resultsMetadata = { hasMoreResults: results.hasMoreResults, itemCount: results.itemCount, diff --git a/webpack.config.js b/webpack.config.js index 21858c2b8..917d4ca87 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -78,11 +78,18 @@ const typescriptRule = { exclude: /node_modules/, }; +const javascriptRule = { + test: /\.m?js$/, + resolve: { + fullySpecified: false, + }, +}; + // eslint-disable-next-line @typescript-eslint/no-unused-vars /** @type {(_env: Record, argv: Record) => import("webpack").Configuration} */ module.exports = function (_env = {}, argv = {}) { const mode = argv.mode || "development"; - const rules = [fontRule, lessRule, imagesRule, cssRule, htmlRule, typescriptRule]; + const rules = [fontRule, lessRule, imagesRule, cssRule, htmlRule, typescriptRule, javascriptRule]; const envVars = { GIT_SHA: gitSha, PORT: process.env.PORT || "1234", From ab4f02f74aa58d8d77ce883707e8df0dbb7640a0 Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Thu, 29 May 2025 10:54:37 -0700 Subject: [PATCH 56/87] Remove frame-src from CSP. (#2169) --- web.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web.config b/web.config index 294afb8d8..8efe7ce6d 100644 --- a/web.config +++ b/web.config @@ -30,7 +30,7 @@ - + From 05d02f08fa526c42b879248e0c05f14ec2688351 Mon Sep 17 00:00:00 2001 From: Dmitry Shilov Date: Tue, 3 Jun 2025 08:47:00 +0200 Subject: [PATCH 57/87] feat: Integrate Fabric Native support for throughput management (#2166) --- .../Controls/ThroughputInput/ThroughputInput.tsx | 4 +++- .../AddCollectionPanel/AddCollectionPanel.tsx | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx index af0219be1..a98803593 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx @@ -11,6 +11,7 @@ import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import * as PricingUtils from "../../../Utils/PricingUtils"; import { CostEstimateText } from "./CostEstimateText/CostEstimateText"; import "./ThroughputInput.less"; +import { isFabricNative } from "../../../Platform/Fabric/FabricUtil"; export interface ThroughputInputProps { isDatabase: boolean; @@ -43,7 +44,8 @@ export const ThroughputInput: FunctionComponent = ({ if ( isFreeTier || isQuickstart || - [Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType) + [Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType) || + isFabricNative() ) { defaultThroughput = AutoPilotUtils.autoPilotThroughput1K; } else if (workloadType === Constants.WorkloadType.Production) { diff --git a/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx index cbbab0192..424df04dc 100644 --- a/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx @@ -60,6 +60,7 @@ import { useDatabases } from "../../useDatabases"; import { PanelFooterComponent } from "../PanelFooterComponent"; import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent"; import { PanelLoadingScreen } from "../PanelLoadingScreen"; +import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; export interface AddCollectionPanelProps { explorer: Explorer; @@ -867,7 +868,7 @@ export class AddCollectionPanel extends React.Component )} - {this.showFullTextSearch && ( + {this.shouldShowFullTextSearchParameters() && ( Date: Wed, 4 Jun 2025 10:27:12 +0530 Subject: [PATCH 58/87] [Supporting the Platform - Azure Cosmos DB- Data Explorer - Graphs]: When page viewport is set to 320x256px, Content under 'Notification' section is not properly visible. (#2161) * [accessibility-3739643]: [Supporting the Platform - Azure Cosmos DB- Data Explorer - Graphs]: When page viewport is set to 320x256px, Content under 'Notification' section is not properly visible. * fix: Adjust notification console styles for better responsiveness. --------- Co-authored-by: Satyapriya Bai --- .../NotificationConsole/NotificationConsole.less | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Explorer/Menus/NotificationConsole/NotificationConsole.less b/src/Explorer/Menus/NotificationConsole/NotificationConsole.less index a03573a72..fb5aed51c 100644 --- a/src/Explorer/Menus/NotificationConsole/NotificationConsole.less +++ b/src/Explorer/Menus/NotificationConsole/NotificationConsole.less @@ -173,8 +173,20 @@ .message { flex-grow: 1; white-space:pre-wrap; + overflow-wrap: break-word; + word-break: break-word; } } } } + + @media (max-width: 768px) { + .notificationConsoleContents { + overflow-y: auto; + + .notificationConsoleData { + overflow: visible; + } + } + } } \ No newline at end of file From 372ac6921f3a22f94248001e9424a460ed8bb645 Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:29:45 +0530 Subject: [PATCH 59/87] fix: Enhance splash screen layout with responsive design for stack elements (#2159) Co-authored-by: Satyapriya Bai --- src/Explorer/SplashScreen/SplashScreen.less | 15 +++++++++++++++ src/Explorer/SplashScreen/SplashScreen.tsx | 10 +++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Explorer/SplashScreen/SplashScreen.less b/src/Explorer/SplashScreen/SplashScreen.less index a8aa3b58d..d146c4fd6 100644 --- a/src/Explorer/SplashScreen/SplashScreen.less +++ b/src/Explorer/SplashScreen/SplashScreen.less @@ -30,6 +30,21 @@ margin: 0px auto; text-align: center; } + .splashStackContainer { + .splashStackRow { + display: flex; + gap: 0 16px; + + @media (max-width: 768px) { + flex-direction: column; + gap: 16px 0; + } + } + + @media (max-width: 768px) { + width: 85% !important; + } + } .mainButtonsContainer { .flex-display(); diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index a922a647a..c0ac6d46c 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -126,8 +126,12 @@ export class SplashScreen extends React.Component { useDatabases.getState().sampleDataResourceTokenCollection ) { return ( - - + + { }} /> - + {useQueryCopilot.getState().copilotEnabled && ( Date: Wed, 4 Jun 2025 11:10:44 +0200 Subject: [PATCH 60/87] fix: Activate the last opened React tab after closing active tab (#2156) --- src/hooks/useTabs.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/hooks/useTabs.ts b/src/hooks/useTabs.ts index 22f4979c5..257f40ac8 100644 --- a/src/hooks/useTabs.ts +++ b/src/hooks/useTabs.ts @@ -103,7 +103,7 @@ export const useTabs: UseStore = create((set, get) => ({ .forEach((tab) => tab.onCloseTabButtonClick()), closeTab: (tab: TabsBase): void => { let tabIndex: number; - const { activeTab, openedTabs } = get(); + const { activeTab, openedTabs, openedReactTabs } = get(); const updatedTabs = openedTabs.filter((openedTab, index) => { if (tab.tabId === openedTab.tabId) { tabIndex = index; @@ -127,6 +127,10 @@ export const useTabs: UseStore = create((set, get) => ({ set({ openedTabs: updatedTabs }); + if (updatedTabs.length === 0 && openedReactTabs.length > 0) { + set({ activeTab: undefined, activeReactTab: openedReactTabs[openedReactTabs.length - 1] }); + } + get().persistTabsState(); }, closeAllNotebookTabs: (hardClose): void => { From 64533b445f2c81d1960e9f87249c823369818751 Mon Sep 17 00:00:00 2001 From: JustinKol <144163838+JustinKol@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:40:57 -0400 Subject: [PATCH 61/87] Autoscale min/max UX changes (#2162) * master pull * Better VSCode detection * Prettier run * Update .npmrc * Update settings.json * Fixed ESLint error * Changed the VSCode detection to a test url that will not open if successful * Initial UX changes to Add Collection Panel * Removing changes from other branch * Reverting explorer changes * Snapshot updates and Lint fixes * Formatting fixes * Setting separator spacing the same * Update test snapshot * Reverting Manual to old UI --- .../CostEstimateText/CostEstimateText.tsx | 44 +- .../ThroughputInput/ThroughputInput.tsx | 127 +- .../ThroughputInput.test.tsx.snap | 2536 ++++++++++++----- src/Explorer/Explorer.tsx | 2 +- .../AddCollectionPanel/AddCollectionPanel.tsx | 29 +- .../AddCollectionPanelUtility.tsx | 17 +- .../AddCollectionPanel.test.tsx.snap | 114 +- 7 files changed, 2040 insertions(+), 829 deletions(-) diff --git a/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx b/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx index 17492691f..4667a1a74 100644 --- a/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx +++ b/src/Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText.tsx @@ -1,4 +1,4 @@ -import { Text } from "@fluentui/react"; +import { Stack, Text } from "@fluentui/react"; import React, { FunctionComponent } from "react"; import { InfoTooltip } from "../../../../Common/Tooltip/InfoTooltip"; import * as SharedConstants from "../../../../Shared/Constants"; @@ -54,28 +54,32 @@ export const CostEstimateText: FunctionComponent = ({ if (isAutoscale) { return ( - - {estimatedMonthlyCost} ({currency}){iconWithEstimatedCostDisclaimer}:{" "} - - {currencySign + calculateEstimateNumber(monthlyPrice / 10)} -{" "} - {currencySign + calculateEstimateNumber(monthlyPrice)}{" "} - - ({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "} - RU/s, {currencySign + pricePerRu}/RU) - + + + {estimatedMonthlyCost} ({currency}){iconWithEstimatedCostDisclaimer}:{" "} + + {currencySign + calculateEstimateNumber(monthlyPrice / 10)} -{" "} + {currencySign + calculateEstimateNumber(monthlyPrice)}{" "} + + ({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "} + RU/s, {currencySign + pricePerRu}/RU) + + ); } return ( - - Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "} - - {currencySign + calculateEstimateNumber(hourlyPrice)} hourly /{" "} - {currencySign + calculateEstimateNumber(dailyPrice)} daily /{" "} - {currencySign + calculateEstimateNumber(monthlyPrice)} monthly{" "} - - ({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "} - {currencySign + pricePerRu}/RU) - + + + Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "} + + {currencySign + calculateEstimateNumber(hourlyPrice)} hourly /{" "} + {currencySign + calculateEstimateNumber(dailyPrice)} daily /{" "} + {currencySign + calculateEstimateNumber(monthlyPrice)} monthly{" "} + + ({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "} + {currencySign + pricePerRu}/RU) + + ); }; diff --git a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx index a98803593..c72925be0 100644 --- a/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx +++ b/src/Explorer/Controls/ThroughputInput/ThroughputInput.tsx @@ -1,5 +1,6 @@ -import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react"; +import { Checkbox, DirectionalHint, Link, Separator, Stack, Text, TextField, TooltipHost } from "@fluentui/react"; import { getWorkloadType } from "Common/DatabaseAccountUtility"; +import { CostEstimateText } from "Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText"; import { useDatabases } from "Explorer/useDatabases"; import React, { FunctionComponent, useEffect, useState } from "react"; import * as Constants from "../../../Common/Constants"; @@ -9,7 +10,6 @@ import { userContext } from "../../../UserContext"; import { getCollectionName } from "../../../Utils/APITypeUtils"; import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import * as PricingUtils from "../../../Utils/PricingUtils"; -import { CostEstimateText } from "./CostEstimateText/CostEstimateText"; import "./ThroughputInput.less"; import { isFabricNative } from "../../../Platform/Fabric/FabricUtil"; @@ -232,53 +232,92 @@ export const ThroughputInput: FunctionComponent = ({
    )} + {isAutoscaleSelected && ( - - Estimate your required RU/s with{" "} - - capacity calculator - - . + + Your container throughput will automatically scale up to the maximum value you select, from a minimum of 10% + of that value. + + + + + Minimum RU/s + + The minimum RU/s your container will scale to + + + {Math.round(throughput / 10).toString()} + + - - - {isDatabase ? "Database" : getCollectionName()} Max RU/s + + x 10 = - {getAutoScaleTooltip()} + + + + + Maximum RU/s + + {getAutoScaleTooltip()} + + onThroughputValueChange(newInput)} + step={AutoPilotUtils.autoPilotIncrementStep} + min={AutoPilotUtils.autoPilotThroughput1K} + max={isSharded ? Number.MAX_SAFE_INTEGER.toString() : "10000"} + value={throughput.toString()} + ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`} + required={true} + errorMessage={throughputError} + /> + - onThroughputValueChange(newInput)} - step={AutoPilotUtils.autoPilotIncrementStep} - min={AutoPilotUtils.autoPilotThroughput1K} - max={isSharded ? Number.MAX_SAFE_INTEGER.toString() : "10000"} - value={throughput.toString()} - ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`} - required={true} - errorMessage={throughputError} - /> - - - Your {isDatabase ? "database" : getCollectionName().toLocaleLowerCase()} throughput will automatically scale - from{" "} - - {AutoPilotUtils.getMinRUsBasedOnUserInput(throughput)} RU/s (10% of max RU/s) - {throughput} RU/s - {" "} - based on usage. - + + + + Estimate your required RU/s with  + + capacity calculator + + . + + + )} @@ -302,7 +341,6 @@ export const ThroughputInput: FunctionComponent = ({ {getAutoScaleTooltip()} - = ({ errorMessage={throughputError} /> + )} - - {throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && ( - Estimate your required RU/s with - - - - - capacity calculator - - - - . + Your container throughput will automatically scale up to the maximum value you select, from a minimum of 10% of that value.
    - +
    + +
    + + + Minimum RU/s + + + + + + +
    + + + +  + + + + +
    +
    +
    +
    +
    +
    +
    + + + 400 + + +
    + + - Container - Max RU/s + x 10 = - - - + - + + + Maximum RU/s + + + + + + +
    + + + +  + + + + +
    +
    +
    +
    +
    +
    +
    + +
    - - - -  - - -
    - - - - +
    +
    +
    +
    - + +
    + + + Estimate your required RU/s with  + + + + capacity calculator + + + + . + + +
    +
    + -
    -
    - -
    -
    + aria-orientation="horizontal" + className="content-135" + role="separator" + />
    -
    -
    - - - Your - container - throughput will automatically scale from - - - 400 - RU/s (10% of max RU/s) - - 4000 - RU/s - - - based on usage. - - + +
    -
    `; diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 23ff72edc..a3685dae9 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -31,6 +31,7 @@ import { readDatabases } from "../Common/dataAccess/readDatabases"; import * as DataModels from "../Contracts/DataModels"; import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse } from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; +import { UploadDetailsRecord } from "../Contracts/ViewModels"; import { GitHubOAuthService } from "../GitHub/GitHubOAuthService"; import { PhoenixClient } from "../Phoenix/PhoenixClient"; import * as ExplorerSettings from "../Shared/ExplorerSettings"; @@ -71,7 +72,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(); diff --git a/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx index 424df04dc..5b7fd1112 100644 --- a/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx @@ -25,7 +25,7 @@ import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullT import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent"; import { AllPropertiesIndexed, - AnalyticalStorageContent, + AnalyticalStoreHeader, ContainerVectorPolicyTooltipContent, FullTextPolicyDefault, getPartitionKey, @@ -265,7 +265,7 @@ export class AddCollectionPanel extends React.Component {!(isFabricNative() && this.props.databaseId !== undefined) && ( -