diff --git a/package-lock.json b/package-lock.json
index 9665339c8..7ed606991 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -130,7 +130,7 @@
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.24.7",
- "@playwright/test": "1.55.1",
+ "@playwright/test": "1.59.1",
"@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56",
@@ -8585,12 +8585,12 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.55.1",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz",
- "integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==",
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
+ "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
"dev": true,
"dependencies": {
- "playwright": "1.55.1"
+ "playwright": "1.59.1"
},
"bin": {
"playwright": "cli.js"
@@ -23953,12 +23953,12 @@
"dev": true
},
"node_modules/playwright": {
- "version": "1.55.1",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz",
- "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==",
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
+ "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
"dev": true,
"dependencies": {
- "playwright-core": "1.55.1"
+ "playwright-core": "1.59.1"
},
"bin": {
"playwright": "cli.js"
@@ -23971,9 +23971,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.55.1",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz",
- "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==",
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
+ "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
diff --git a/package.json b/package.json
index d952483f9..116612d6a 100644
--- a/package.json
+++ b/package.json
@@ -140,7 +140,7 @@
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.24.7",
- "@playwright/test": "1.55.1",
+ "@playwright/test": "1.59.1",
"@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56",
diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts
index ab179f0ee..0a51d721a 100644
--- a/src/Contracts/DataModels.ts
+++ b/src/Contracts/DataModels.ts
@@ -347,6 +347,7 @@ export interface Offer {
export interface ThroughputBucket {
id: number;
maxThroughputPercentage: number;
+ isDefaultBucket?: boolean;
}
export interface SDKOfferDefinition extends Resource {
diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.test.tsx
index 2ad5c819e..08bc60e40 100644
--- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.test.tsx
+++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.test.tsx
@@ -76,11 +76,11 @@ describe("ThroughputBucketsComponent", () => {
fireEvent.change(input, { target: { value: "70" } });
expect(mockOnBucketsChange).toHaveBeenCalledWith([
- { id: 1, maxThroughputPercentage: 70 },
- { id: 2, maxThroughputPercentage: 60 },
- { id: 3, maxThroughputPercentage: 100 },
- { id: 4, maxThroughputPercentage: 100 },
- { id: 5, maxThroughputPercentage: 100 },
+ { id: 1, maxThroughputPercentage: 70, isDefaultBucket: false },
+ { id: 2, maxThroughputPercentage: 60, isDefaultBucket: false },
+ { id: 3, maxThroughputPercentage: 100, isDefaultBucket: false },
+ { id: 4, maxThroughputPercentage: 100, isDefaultBucket: false },
+ { id: 5, maxThroughputPercentage: 100, isDefaultBucket: false },
]);
});
@@ -102,11 +102,11 @@ describe("ThroughputBucketsComponent", () => {
fireEvent.change(input2, { target: { value: "80" } });
expect(mockOnBucketsChange).toHaveBeenCalledWith([
- { id: 1, maxThroughputPercentage: 70 },
- { id: 2, maxThroughputPercentage: 80 },
- { id: 3, maxThroughputPercentage: 100 },
- { id: 4, maxThroughputPercentage: 100 },
- { id: 5, maxThroughputPercentage: 100 },
+ { id: 1, maxThroughputPercentage: 70, isDefaultBucket: false },
+ { id: 2, maxThroughputPercentage: 80, isDefaultBucket: false },
+ { id: 3, maxThroughputPercentage: 100, isDefaultBucket: false },
+ { id: 4, maxThroughputPercentage: 100, isDefaultBucket: false },
+ { id: 5, maxThroughputPercentage: 100, isDefaultBucket: false },
]);
});
@@ -134,8 +134,8 @@ describe("ThroughputBucketsComponent", () => {
,
);
@@ -157,21 +157,21 @@ describe("ThroughputBucketsComponent", () => {
fireEvent.click(toggles[0]);
expect(mockOnBucketsChange).toHaveBeenCalledWith([
- { id: 1, maxThroughputPercentage: 100 },
- { id: 2, maxThroughputPercentage: 60 },
- { id: 3, maxThroughputPercentage: 100 },
- { id: 4, maxThroughputPercentage: 100 },
- { id: 5, maxThroughputPercentage: 100 },
+ { id: 1, maxThroughputPercentage: 100, isDefaultBucket: false },
+ { id: 2, maxThroughputPercentage: 60, isDefaultBucket: false },
+ { id: 3, maxThroughputPercentage: 100, isDefaultBucket: false },
+ { id: 4, maxThroughputPercentage: 100, isDefaultBucket: false },
+ { id: 5, maxThroughputPercentage: 100, isDefaultBucket: false },
]);
fireEvent.click(toggles[0]);
expect(mockOnBucketsChange).toHaveBeenCalledWith([
- { id: 1, maxThroughputPercentage: 50 },
- { id: 2, maxThroughputPercentage: 60 },
- { id: 3, maxThroughputPercentage: 100 },
- { id: 4, maxThroughputPercentage: 100 },
- { id: 5, maxThroughputPercentage: 100 },
+ { id: 1, maxThroughputPercentage: 50, isDefaultBucket: false },
+ { id: 2, maxThroughputPercentage: 60, isDefaultBucket: false },
+ { id: 3, maxThroughputPercentage: 100, isDefaultBucket: false },
+ { id: 4, maxThroughputPercentage: 100, isDefaultBucket: false },
+ { id: 5, maxThroughputPercentage: 100, isDefaultBucket: false },
]);
});
diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.tsx
index 6d2bb9eb2..f81b7c2f9 100644
--- a/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.tsx
+++ b/src/Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent.tsx
@@ -1,4 +1,16 @@
-import { Label, Slider, Stack, TextField, Toggle } from "@fluentui/react";
+import {
+ Dropdown,
+ Icon,
+ IDropdownOption,
+ Label,
+ Link,
+ Slider,
+ Stack,
+ Text,
+ TextField,
+ Toggle,
+ TooltipHost,
+} from "@fluentui/react";
import { ThroughputBucket } from "Contracts/DataModels";
import { Keys, t } from "Localization";
import React, { FC, useEffect, useState } from "react";
@@ -9,6 +21,7 @@ const MAX_BUCKET_SIZES = 5;
const DEFAULT_BUCKETS = Array.from({ length: MAX_BUCKET_SIZES }, (_, i) => ({
id: i + 1,
maxThroughputPercentage: 100,
+ isDefaultBucket: false,
}));
export interface ThroughputBucketsComponentProps {
@@ -24,19 +37,54 @@ export const ThroughputBucketsComponent: FC = (
onBucketsChange,
onSaveableChange,
}) => {
+ const NoDefaultThroughputSelectedKey: number = -1;
const getThroughputBuckets = (buckets: ThroughputBucket[]): ThroughputBucket[] => {
if (!buckets || buckets.length === 0) {
return DEFAULT_BUCKETS;
}
const maxBuckets = Math.max(DEFAULT_BUCKETS.length, buckets.length);
- const adjustedDefaultBuckets = Array.from({ length: maxBuckets }, (_, i) => ({
- id: i + 1,
- maxThroughputPercentage: 100,
+ const adjustedDefaultBuckets: ThroughputBucket[] = Array.from(
+ { length: maxBuckets },
+ (_, i) =>
+ ({
+ id: i + 1,
+ maxThroughputPercentage: 100,
+ isDefaultBucket: false,
+ }) as ThroughputBucket,
+ );
+
+ return adjustedDefaultBuckets.map((defaultBucket: ThroughputBucket) => {
+ const incoming: ThroughputBucket = buckets?.find((bucket) => bucket.id === defaultBucket.id);
+
+ return {
+ ...defaultBucket,
+ ...incoming,
+ ...(incoming?.isDefaultBucket && { isDefaultBucket: true }),
+ };
+ });
+ };
+
+ const getThroughputBucketOptions = (): IDropdownOption[] => {
+ const noDefaultThroughputBucketSelected: IDropdownOption = {
+ key: NoDefaultThroughputSelectedKey,
+ text: t(Keys.controls.settings.throughputBuckets.noDefaultBucketSelected),
+ };
+
+ const throughputBucketOptions: IDropdownOption[] = throughputBuckets.map((bucket) => ({
+ key: bucket.id,
+ text: t(Keys.controls.settings.throughputBuckets.bucketOptionLabel, {
+ id: String(bucket.id),
+ percentage: String(bucket.maxThroughputPercentage),
+ }),
+ disabled: bucket.maxThroughputPercentage === 100,
+ ...(bucket.maxThroughputPercentage === 100 && {
+ data: {
+ tooltip: t(Keys.controls.settings.throughputBuckets.bucketNotActive, { id: String(bucket.id) }),
+ },
+ }),
}));
- return adjustedDefaultBuckets.map(
- (defaultBucket) => buckets?.find((bucket) => bucket.id === defaultBucket.id) || defaultBucket,
- );
+ return [noDefaultThroughputBucketSelected, ...throughputBucketOptions];
};
const [throughputBuckets, setThroughputBuckets] = useState(getThroughputBuckets(currentBuckets));
@@ -53,7 +101,13 @@ export const ThroughputBucketsComponent: FC = (
const handleBucketChange = (id: number, newValue: number) => {
const updatedBuckets = throughputBuckets.map((bucket) =>
- bucket.id === id ? { ...bucket, maxThroughputPercentage: newValue } : bucket,
+ bucket.id === id
+ ? {
+ ...bucket,
+ maxThroughputPercentage: newValue,
+ isDefaultBucket: newValue === 100 ? false : bucket.isDefaultBucket,
+ }
+ : bucket,
);
setThroughputBuckets(updatedBuckets);
const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
@@ -64,6 +118,38 @@ export const ThroughputBucketsComponent: FC = (
handleBucketChange(id, checked ? 50 : 100);
};
+ const onDefaultBucketToggle = (id: number, checked: boolean): void => {
+ const updatedBuckets: ThroughputBucket[] = throughputBuckets.map((bucket) =>
+ bucket.id === id ? { ...bucket, isDefaultBucket: checked } : { ...bucket, isDefaultBucket: false },
+ );
+ setThroughputBuckets(updatedBuckets);
+ const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
+ settingsChanged && onBucketsChange(updatedBuckets);
+ };
+
+ const onRenderDefaultThroughputBucketLabel = (): JSX.Element => {
+ const tooltipContent = (): JSX.Element => (
+
+ {t(Keys.controls.settings.throughputBuckets.defaultBucketTooltip)}{" "}
+
+ {t(Keys.controls.settings.throughputBuckets.defaultBucketTooltipLearnMore)}
+
+
+ );
+
+ return (
+
+
+
+
+
+
+ );
+ };
+
return (
))}
+ throughputbucket.isDefaultBucket)?.id ||
+ NoDefaultThroughputSelectedKey
+ }
+ onChange={(_, option) => onDefaultBucketToggle(option.key as number, true)}
+ onRenderLabel={onRenderDefaultThroughputBucketLabel}
+ onRenderOption={(option: IDropdownOption) => {
+ const tooltip: string = option?.data?.tooltip;
+ if (!tooltip) {
+ return <>{option?.text}>;
+ }
+
+ return (
+
+ {option?.text}
+
+ );
+ }}
+ styles={{ root: { width: "50%" } }}
+ data-test="default-throughput-bucket-dropdown"
+ />
);
};
diff --git a/src/Localization/en/Resources.json b/src/Localization/en/Resources.json
index 7723aa709..14dd422c1 100644
--- a/src/Localization/en/Resources.json
+++ b/src/Localization/en/Resources.json
@@ -961,7 +961,14 @@
"bucketLabel": "Bucket {{id}}",
"dataExplorerQueryBucket": " (Data Explorer Query Bucket)",
"active": "Active",
- "inactive": "Inactive"
+ "inactive": "Inactive",
+ "defaultBucketLabel": "Default Throughput Bucket",
+ "defaultBucketPlaceholder": "Select a default throughput bucket",
+ "defaultBucketTooltip": "The default throughput bucket is used for operations that do not specify a particular bucket.",
+ "defaultBucketTooltipLearnMore": "Learn more.",
+ "noDefaultBucketSelected": "No Default Throughput Bucket Selected",
+ "bucketOptionLabel": "Bucket {{id}} - {{percentage}}%",
+ "bucketNotActive": "Bucket {{id}} is not active."
}
}
}
diff --git a/test/sql/containercopy/permissionsScreen.spec.ts b/test/sql/containercopy/permissionsScreen.spec.ts
index f592bf4c7..a36dc0aaf 100644
--- a/test/sql/containercopy/permissionsScreen.spec.ts
+++ b/test/sql/containercopy/permissionsScreen.spec.ts
@@ -20,7 +20,7 @@ test.describe("Container Copy - Permission Screen Verification", () => {
});
test.afterEach("Cleanup after each test", async () => {
- await page.unroute(/.*/, (route) => route.continue());
+ await page.unrouteAll({ behavior: "ignoreErrors" });
await page.close();
});
diff --git a/test/sql/scaleAndSettings/computedProperties.spec.ts b/test/sql/scaleAndSettings/computedProperties.spec.ts
index d1f95e53e..cc3d33f17 100644
--- a/test/sql/scaleAndSettings/computedProperties.spec.ts
+++ b/test/sql/scaleAndSettings/computedProperties.spec.ts
@@ -23,7 +23,9 @@ test.describe("Computed Properties", () => {
});
test.afterAll("Delete Test Database", async () => {
- await context?.dispose();
+ if (!process.env.CI) {
+ await context?.dispose();
+ }
});
test("Add valid computed property", async ({ page }) => {
diff --git a/test/sql/scaleAndSettings/throughputbucket.spec.ts b/test/sql/scaleAndSettings/throughputbucket.spec.ts
new file mode 100644
index 000000000..1753c9b2e
--- /dev/null
+++ b/test/sql/scaleAndSettings/throughputbucket.spec.ts
@@ -0,0 +1,108 @@
+import { expect, test } from "@playwright/test";
+import { CommandBarButton, DataExplorer, ONE_MINUTE_MS, TestAccount } from "../../fx";
+import { createTestSQLContainer, TestContainerContext } from "../../testData";
+
+test.describe("Throughput bucket settings", () => {
+ let context: TestContainerContext = null!;
+ let explorer: DataExplorer = null!;
+
+ test.beforeAll("Create Test Database", async () => {
+ context = await createTestSQLContainer();
+ });
+
+ test.beforeEach("Open Throughput Bucket Settings", async ({ browser }) => {
+ const page = await browser.newPage();
+ explorer = await DataExplorer.open(page, TestAccount.SQL);
+
+ // Click Scale & Settings and open Throughput Bucket Settings tab
+ await explorer.openScaleAndSettings(context);
+ const throughputBucketTab = explorer.frame.getByTestId("settings-tab-header/ThroughputBucketsTab");
+ await throughputBucketTab.click();
+ });
+
+ // Delete database only if not running in CI
+ if (!process.env.CI) {
+ test.afterAll("Delete Test Database", async () => {
+ await context?.dispose();
+ });
+ }
+
+ test("Activate throughput bucket #2", async () => {
+ // Activate bucket 2
+ const bucket2Toggle = explorer.frame.getByTestId("bucket-2-active-toggle");
+ await bucket2Toggle.click();
+
+ await explorer.commandBarButton(CommandBarButton.Save).click();
+ await expect(explorer.getConsoleHeaderStatus()).toContainText(
+ `Successfully updated offer for collection ${context.container.id}`,
+ {
+ timeout: 2 * ONE_MINUTE_MS,
+ },
+ );
+ });
+
+ test("Activate throughput buckets #1 and #2", async () => {
+ // Activate bucket 1
+ const bucket1Toggle = explorer.frame.getByTestId("bucket-1-active-toggle");
+ await bucket1Toggle.click();
+
+ // Activate bucket 2
+ const bucket2Toggle = explorer.frame.getByTestId("bucket-2-active-toggle");
+ await bucket2Toggle.click();
+ await explorer.commandBarButton(CommandBarButton.Save).click();
+ await expect(explorer.getConsoleHeaderStatus()).toContainText(
+ `Successfully updated offer for collection ${context.container.id}`,
+ {
+ timeout: 2 * ONE_MINUTE_MS,
+ },
+ );
+ });
+
+ test("Set throughput percentage for bucket #1", async () => {
+ // Set throughput percentage for bucket 1 (inactive) - Should be disabled
+ const bucket1PercentageInput = explorer.frame.getByTestId("bucket-1-percentage-input");
+ expect(bucket1PercentageInput).toBeDisabled();
+
+ // Activate bucket 1
+ const bucket1Toggle = explorer.frame.getByTestId("bucket-1-active-toggle");
+ await bucket1Toggle.click();
+ expect(bucket1PercentageInput).toBeEnabled();
+ await bucket1PercentageInput.fill("40");
+
+ await explorer.commandBarButton(CommandBarButton.Save).click();
+ await expect(explorer.getConsoleHeaderStatus()).toContainText(
+ `Successfully updated offer for collection ${context.container.id}`,
+ {
+ timeout: 2 * ONE_MINUTE_MS,
+ },
+ );
+ });
+
+ test("Set default throughput bucket", async () => {
+ // There are no active throughput buckets so they all should be disabled
+ const defaultThroughputBucketDropdown = explorer.frame.getByTestId("default-throughput-bucket-dropdown");
+ await defaultThroughputBucketDropdown.click();
+
+ const bucket1Option = explorer.frame.getByRole("option", { name: "Bucket 1" });
+ expect(bucket1Option).toBeDisabled();
+
+ // Activate bucket 1
+ const bucket1Toggle = explorer.frame.getByTestId("bucket-1-active-toggle");
+ await bucket1Toggle.click();
+
+ // Open dropdown again
+ await defaultThroughputBucketDropdown.click();
+ expect(bucket1Option).toBeEnabled();
+
+ // Select bucket 1 as default
+ await bucket1Option.click();
+
+ await explorer.commandBarButton(CommandBarButton.Save).click();
+ await expect(explorer.getConsoleHeaderStatus()).toContainText(
+ `Successfully updated offer for collection ${context.container.id}`,
+ {
+ timeout: 2 * ONE_MINUTE_MS,
+ },
+ );
+ });
+});
diff --git a/test/testData.ts b/test/testData.ts
index 494ce2e5d..e9ba759f1 100644
--- a/test/testData.ts
+++ b/test/testData.ts
@@ -223,10 +223,15 @@ export async function createTestSQLContainer({
const { database } = await client.databases.createIfNotExists({ id: databaseId });
try {
- const { container } = await database.containers.createIfNotExists({
- id: containerId,
- partitionKey,
- });
+ const { container } = await database.containers.createIfNotExists(
+ {
+ id: containerId,
+ partitionKey,
+ },
+ {
+ offerThroughput: 4000,
+ },
+ );
if (includeTestData) {
const batchCount = TestData.length / 100;
for (let i = 0; i < batchCount; i++) {