diff --git a/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx index 4a1009ba2..f578ab81b 100644 --- a/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel/AddCollectionPanel.tsx @@ -7,6 +7,7 @@ import { Icon, IconButton, IDropdownOption, + IRenderFunction, Link, ProgressIndicator, Separator, @@ -410,6 +411,7 @@ export class AddCollectionPanel extends React.Component )} @@ -720,8 +722,8 @@ export class AddCollectionPanel extends React.Component @@ -731,8 +733,8 @@ export class AddCollectionPanel extends React.Component @@ -911,8 +913,8 @@ export class AddCollectionPanel extends React.Component { scrollToSection("collapsibleFullTextPolicySectionContent"); }} - //TODO: uncomment when learn more text becomes available - // tooltipContent={this.getContainerFullTextPolicyTooltipContent()} + //TODO: uncomment when learn more text becomes available + // tooltipContent={this.getContainerFullTextPolicyTooltipContent()} > @@ -1340,15 +1342,15 @@ export class AddCollectionPanel extends React.Component 0 - ? this.state.subPartitionKeys - : []), - ], - kind: userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0 ? "MultiHash" : "Hash", - version: partitionKeyVersion, - } + paths: [ + partitionKeyString, + ...(userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0 + ? this.state.subPartitionKeys + : []), + ], + kind: userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0 ? "MultiHash" : "Hash", + version: partitionKeyVersion, + } : undefined; const indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing @@ -1473,4 +1475,19 @@ export class AddCollectionPanel extends React.Component JSX.Element, + ): JSX.Element | null => { + if (!option) { + return null; + } + + return ( +
+ {defaultRender ? defaultRender(option) : {option.text}} +
+ ); + }; } diff --git a/test/fx.ts b/test/fx.ts index 56e571635..5bf6aa6e3 100644 --- a/test/fx.ts +++ b/test/fx.ts @@ -1,7 +1,7 @@ import { DefaultAzureCredential } from "@azure/identity"; import { Frame, Locator, Page, expect } from "@playwright/test"; import crypto from "crypto"; -import { TestContainerContext } from "./testData"; +import { TestContainerContext, TestDatabaseContext } from "./testData"; const RETRY_COUNT = 3; @@ -56,7 +56,9 @@ export const defaultAccounts: Record = { export const resourceGroupName = process.env.DE_TEST_RESOURCE_GROUP ?? "de-e2e-tests"; export const subscriptionId = process.env.DE_TEST_SUBSCRIPTION_ID ?? "69e02f2d-f059-4409-9eac-97e8a276ae2c"; export const TEST_AUTOSCALE_THROUGHPUT_RU = 1000; +export const TEST_MANUAL_THROUGHPUT_RU = 800; export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_2K = 2000; +export const TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K = 4000; export const TEST_MANUAL_THROUGHPUT_RU_2K = 2000; export const ONE_MINUTE_MS: number = 60 * 1000; diff --git a/test/sql/scaleAndSettings/sharedThroughput.spec.ts b/test/sql/scaleAndSettings/sharedThroughput.spec.ts new file mode 100644 index 000000000..8676dfdf8 --- /dev/null +++ b/test/sql/scaleAndSettings/sharedThroughput.spec.ts @@ -0,0 +1,288 @@ +import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; +import { CosmosClient, CosmosClientOptions, Database } from "@azure/cosmos"; +import { AzureIdentityCredentialAdapter } from "@azure/ms-rest-js"; +import { Locator, expect, test } from "@playwright/test"; +import { + CommandBarButton, + DataExplorer, + ONE_MINUTE_MS, + TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K, + TEST_MANUAL_THROUGHPUT_RU, + TestAccount, + generateUniqueName, + getAccountName, + getAzureCLICredentials, + resourceGroupName, + subscriptionId, +} from "../../fx"; + +// Helper class for database context +class TestDatabaseContext { + constructor( + public armClient: CosmosDBManagementClient, + public client: CosmosClient, + public database: Database, + ) {} + + async dispose() { + await this.database.delete(); + } +} + +// Options for creating test database +interface CreateTestDBOptions { + throughput?: number; + maxThroughput?: number; // For autoscale +} + +// Helper function to create a test database with shared throughput +async function createTestDB(options?: CreateTestDBOptions): Promise { + const databaseId = generateUniqueName("db"); + const credentials = getAzureCLICredentials(); + const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials); + const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId); + const accountName = getAccountName(TestAccount.SQL); + const account = await armClient.databaseAccounts.get(resourceGroupName, accountName); + + const clientOptions: CosmosClientOptions = { + endpoint: account.documentEndpoint!, + }; + + const nosqlAccountRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN; + if (nosqlAccountRbacToken) { + clientOptions.tokenProvider = async (): Promise => { + const AUTH_PREFIX = `type=aad&ver=1.0&sig=`; + const authorizationToken = `${AUTH_PREFIX}${nosqlAccountRbacToken}`; + return authorizationToken; + }; + } else { + const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName); + clientOptions.key = keys.primaryMasterKey; + } + + const client = new CosmosClient(clientOptions); + + // Create database with provisioned throughput (shared throughput) + // This checks the "Provision database throughput" option + const { database } = await client.databases.create({ + id: databaseId, + throughput: options?.throughput, // Manual throughput (e.g., 400) + maxThroughput: options?.maxThroughput, // Autoscale max throughput (e.g., 1000) + }); + + return new TestDatabaseContext(armClient, client, database); +} + +test.describe("Database with Shared Throughput", () => { + let dbContext: TestDatabaseContext = null!; + let explorer: DataExplorer = null!; + const containerId = "sharedcontainer"; + + // Helper methods + const getThroughputInput = (type: "manual" | "autopilot"): Locator => { + return explorer.frame.getByTestId(`${type}-throughput-input`); + }; + + test.afterEach(async () => { + // Clean up: delete the created database + await dbContext?.dispose(); + }); + + test.describe("Manual Throughput Tests", () => { + test.beforeEach(async ({ page }) => { + explorer = await DataExplorer.open(page, TestAccount.SQL); + }); + + test("Create database with shared manual throughput and verify Scale node in UI", async () => { + test.setTimeout(120000); // 2 minutes timeout + // Create database with shared manual throughput (400 RU/s) + dbContext = await createTestDB({ throughput: 400 }); + + // Verify database node appears in the tree + const databaseNode = await explorer.waitForNode(dbContext.database.id); + expect(databaseNode).toBeDefined(); + + // Expand the database node to see child nodes + await databaseNode.expand(); + + // Verify that "Scale" node appears under the database + const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); + expect(scaleNode).toBeDefined(); + await expect(scaleNode.element).toBeVisible(); + }); + + test("Add container to shared database without dedicated throughput", async () => { + // Create database with shared manual throughput + dbContext = await createTestDB({ throughput: 400 }); + + // Wait for the database to appear in the tree + await explorer.waitForNode(dbContext.database.id); + + // Add a container to the shared database via UI + await explorer.globalCommandButton("New Container").click(); + + await explorer.whilePanelOpen( + "New Container", + async (panel, okButton) => { + // Select "Use existing" database + const useExistingRadio = panel.getByRole("radio", { name: /Use existing/i }); + await useExistingRadio.click(); + + // Select the database from dropdown using the new data-testid + const databaseDropdown = panel.getByRole("combobox", { name: "Choose an existing database" }); + await databaseDropdown.click(); + + await explorer.frame.getByRole("option", { name: dbContext.database.id }).click(); + // Now you can target the specific database option by its data-testid + //await panel.getByTestId(`database-option-${dbContext.database.id}`).click(); + // Fill container id + await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId); + + // Fill partition key + await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk"); + + // Ensure "Provision dedicated throughput" is NOT checked + const dedicatedThroughputCheckbox = panel.getByRole("checkbox", { + name: /Provision dedicated throughput for this container/i, + }); + + if (await dedicatedThroughputCheckbox.isVisible()) { + const isChecked = await dedicatedThroughputCheckbox.isChecked(); + if (isChecked) { + await dedicatedThroughputCheckbox.uncheck(); + } + } + + await okButton.click(); + }, + { closeTimeout: 5 * ONE_MINUTE_MS }, + ); + + // Verify container was created under the database + const containerNode = await explorer.waitForContainerNode(dbContext.database.id, containerId); + expect(containerNode).toBeDefined(); + }); + + test("Scale shared database manual throughput", async () => { + // Create database with shared manual throughput (400 RU/s) + dbContext = await createTestDB({ throughput: 400 }); + + // Navigate to the scale settings by clicking the "Scale" node in the tree + const databaseNode = await explorer.waitForNode(dbContext.database.id); + await databaseNode.expand(); + const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); + await scaleNode.element.click(); + + // Update manual throughput from 400 to 800 + await getThroughputInput("manual").fill(TEST_MANUAL_THROUGHPUT_RU.toString()); + + // Save changes + await explorer.commandBarButton(CommandBarButton.Save).click(); + + // Verify success message + await expect(explorer.getConsoleMessage()).toContainText( + `Successfully updated offer for database ${dbContext.database.id}`, + { timeout: 2 * ONE_MINUTE_MS }, + ); + }); + + test("Scale shared database from manual to autoscale", async () => { + // Create database with shared manual throughput (400 RU/s) + dbContext = await createTestDB({ throughput: 400 }); + + // Open database settings by clicking the "Scale" node + const databaseNode = await explorer.waitForNode(dbContext.database.id); + await databaseNode.expand(); + const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); + await scaleNode.element.click(); + + // Switch to Autoscale + const autoscaleRadio = explorer.frame.getByText("Autoscale", { exact: true }); + await autoscaleRadio.click(); + + // Set autoscale max throughput to 1000 + //await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString()); + + // Save changes + await explorer.commandBarButton(CommandBarButton.Save).click(); + + // Verify success message + await expect(explorer.getConsoleMessage()).toContainText( + `Successfully updated offer for database ${dbContext.database.id}`, + { timeout: 2 * ONE_MINUTE_MS }, + ); + }); + }); + + test.describe("Autoscale Throughput Tests", () => { + test.beforeEach(async ({ page }) => { + explorer = await DataExplorer.open(page, TestAccount.SQL); + }); + + test("Create database with shared autoscale throughput and verify in UI", async () => { + test.setTimeout(120000); // 2 minutes timeout + + // Create database with shared autoscale throughput (max 1000 RU/s) + dbContext = await createTestDB({ maxThroughput: 1000 }); + + // Verify database node appears + const databaseNode = await explorer.waitForNode(dbContext.database.id); + expect(databaseNode).toBeDefined(); + + // Expand the database node to see child nodes + await databaseNode.expand(); + + // Verify that "Scale" node appears under the database + const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); + expect(scaleNode).toBeDefined(); + await expect(scaleNode.element).toBeVisible(); + }); + + test("Scale shared database autoscale throughput", async () => { + // Create database with shared autoscale throughput (max 1000 RU/s) + dbContext = await createTestDB({ maxThroughput: 1000 }); + + // Open database settings + const databaseNode = await explorer.waitForNode(dbContext.database.id); + await databaseNode.expand(); + const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); + await scaleNode.element.click(); + + // Update autoscale max throughput from 1000 to 4000 + await getThroughputInput("autopilot").fill(TEST_AUTOSCALE_MAX_THROUGHPUT_RU_4K.toString()); + + // Save changes + await explorer.commandBarButton(CommandBarButton.Save).click(); + + // Verify success message + await expect(explorer.getConsoleMessage()).toContainText( + `Successfully updated offer for database ${dbContext.database.id}`, + { timeout: 2 * ONE_MINUTE_MS }, + ); + }); + + test("Scale shared database from autoscale to manual", async () => { + // Create database with shared autoscale throughput (max 1000 RU/s) + dbContext = await createTestDB({ maxThroughput: 1000 }); + + // Open database settings + const databaseNode = await explorer.waitForNode(dbContext.database.id); + await databaseNode.expand(); + const scaleNode = await explorer.waitForNode(`${dbContext.database.id}/Scale`); + await scaleNode.element.click(); + + // Switch to Manual + const manualRadio = explorer.frame.getByText("Manual", { exact: true }); + await manualRadio.click(); + + // Save changes + await explorer.commandBarButton(CommandBarButton.Save).click(); + + // Verify success message + await expect(explorer.getConsoleMessage()).toContainText( + `Successfully updated offer for database ${dbContext.database.id}`, + { timeout: 2 * ONE_MINUTE_MS }, + ); + }); + }); +}); \ No newline at end of file diff --git a/test/testData.ts b/test/testData.ts index 94d38941f..4e283f50e 100644 --- a/test/testData.ts +++ b/test/testData.ts @@ -74,6 +74,60 @@ export class TestContainerContext { } } +export class TestDatabaseContext { + constructor( + public armClient: CosmosDBManagementClient, + public client: CosmosClient, + public database: Database, + ) {} + + async dispose() { + await this.database.delete(); + } +} + +export interface CreateTestDBOptions { + throughput?: number; + maxThroughput?: number; // For autoscale +} + +export async function createTestDB(options?: CreateTestDBOptions): Promise { + const databaseId = generateUniqueName("db"); + const credentials = getAzureCLICredentials(); + const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials); + const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId); + const accountName = getAccountName(TestAccount.SQL); + const account = await armClient.databaseAccounts.get(resourceGroupName, accountName); + + const clientOptions: CosmosClientOptions = { + endpoint: account.documentEndpoint!, + }; + + const nosqlAccountRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN; + if (nosqlAccountRbacToken) { + clientOptions.tokenProvider = async (): Promise => { + const AUTH_PREFIX = `type=aad&ver=1.0&sig=`; + const authorizationToken = `${AUTH_PREFIX}${nosqlAccountRbacToken}`; + return authorizationToken; + }; + } else { + const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName); + clientOptions.key = keys.primaryMasterKey; + } + + const client = new CosmosClient(clientOptions); + + // Create database with provisioned throughput (shared throughput) + // This checks the "Provision database throughput" option + const { database } = await client.databases.create({ + id: databaseId, + throughput: options?.throughput, // Manual throughput (e.g., 400) + maxThroughput: options?.maxThroughput, // Autoscale max throughput (e.g., 1000) + }); + + return new TestDatabaseContext(armClient, client, database); +} + export async function createTestSQLContainer(includeTestData?: boolean) { const databaseId = generateUniqueName("db"); const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique